在 C# 异步方法中使用 out/ref 参数机制
来源:互联网 发布:苹果系统mac系统下载 编辑:程序博客网 时间:2024/05/21 09:19
由于异步方法的特殊性,.NET 不允许异步方法带有 out 或 ref 参数。事实上这个设计是合理的,因为异步方法几乎都是立即返回,而此时传入方法内的 out
/ref
参数可能还没有被赋值。那么,如果确实需要在异步方法内用到类似于 out
/ref
的按引用传参的机制怎么办呢?
为什么需要 out
/ref
?
简言之,为了让调用方法能够返回多个值。很常见的情况是,除了一般的返回值,我们还需要知道其他结果。例如,bool int.TryParse(String,out int)
这个方法用来将字符串转换为对于的 32 位整数,除了返回 true
/false
表示转换是否成功之外,我们还需要知道转换后的值,于是就加了个 out
参数来存储这个值。(题外话:这应该算是.NET 的历史遗留问题,如果可空值类型一早就出现在 .NET 里,可能这个方法就不会这样难看了,签名完全可以变成更简洁的形式:int? int.TryParse(String)
。)
示例
假设一个接口 IFileSystemProvider
提供了异步方法 ReadAsync
。这个接口方法抽象了对文件系统的读取操作。调用时,需要提供一个标识文件资源的字符串。方法完成后,从这个方法中获得该文件的流、文件名和一些元数据(MD5 哈希值、原始文件名、创建时间等)。元数据用一张字典存储。这样,从一个方法中,我们需要给出一个输入,并获得三个输出。
如果这个方法是同步的,那么我们可以不假思索地这么写下签名:
Stream Read(string resource, out string originalFileName, out IDictionary<string, object> metadata);
但异步方法不能使用 out
/ref
参数,此时又该如何定义这个接口方法的签名?
【方法 1】 使用一个类或结构体包含返回值
可以写一个不变的类或结构体,来封装这些返回值。
public sealed class ReadResult{ public ReadResult(Stream stream, string originalFileName, IDictionary<string, object> metadata) { this.Stream = stream; this.OriginalFileName = originalFileName; this.Metadata = metadata; } public Stream Stream { get; } public string OriginalFileName { get; } public IDictionary<string, object> Metadata { get; }}
【优点】
对调用方友好,代码的可读性和可维护性极强。
【缺点】
代码量过多。这种一次性使用的封装类(只是为一个方法使用的类)会导致代码库显得臃肿不堪。在一些人严重或许不算缺陷,但对于开发求快,惜字如金(当然是在不影响性能的情况下)的 .NET 开发者而言,这可算是个无法忍受的问题。
另外,过多临时封装类的频繁创建还会导致内存分配和 GC 的压力增大,对性能产生影响。
【方法 2】 使用元组 Tuple
该方法旨在缓解方法 1 的代码臃肿的问题。元组可以包含一个或多个值,直接用元组包括并返回你需要的对象:
Task<Tuple<Stream, string, IDictionary<string, object>>> ReadAsync(string resource, CancellationToken cancellationToken = default(CancellationToken));
【优点】
很简单,不用额外的代码。
【缺点】
可读性差。元组里的每一项的含义不明确(只能通过访问返回的元组的 Item1
、Item2
属性这样的方式获得),如果要返回参数很多,一定要有良好的注释,说明元组每一项的含义。
【方法 3】利用委托回调(Lambda 表达式)
Task<Stream> ReadAsync(string resource, Action<string, IDictionary<string, object>> fileNameMetadataAccessor = null, CancellationToken cancellationToken = default(CancellationToken));
其本质就是回调函数。实现方的代码在异步方法完成时,将文件名和元数据作为参数,传递给 fileNameMetadataAccessor
委托并调用。
实现方的示例代码:
public async Task<Stream> ReadAsync(string resource, Action<string, IDictionary<string, object>> fileNameMetadataAccessor = null, CancellationToken cancellationToken = default(CancellationToken)){ ... var stream = await dfsClient.ReadAsync(); var entry = await dbContext.FileEntries.FirstOrDefaultAsync(c => c.Resource == resource); if(entry != null) { fileNameMetadataAccessor?.Invoke(entry.OriginalFleName, JSONHelper.AsDictionary(entry.Metadata)); } ...}
调用方的示例代码:
IFileSystemProvider provider = ...;string resource = ...;string originalFileName = null;IDictionary<string, object> metadata = null;var stream = await provider.ReadAsync(resource, (o, m) => { originalFileName = o; metadata = m;});...
【优点】
简洁优雅,不用额外的代码。
【缺点】
如果要返回的值较多,可读性就较差;同样也要有良好的注释,说明 Action
里每一个参数的含义。(可以自定义一个委托类型而不是使用通用的 Action
委托来规避可读性差的问题,但这也会导致代码显得过于臃肿)
同时,这样做也有隐含的调用风险:因为是异步方法,传入的委托可能还没有被调用,如果委托影响了在 lambda 方法作用域外的状态,就可能出现问题。(传入的委托应该更多地视作常规的回调函数,而不应该是“设置返回值”的函数,因为这样的函数很容易就影响了闭包状态)
例如,在上面的调用方代码里的 await
去掉。
var streamTask = provider.ReadAsync(resource, (o, m) => { originalFileName = o; metadata = m;});Console.WriteLine(metadata["FileSize"]);var stream = await streamTask;...
这会在最后一行引发 NullReferenceException
异常,因为 ReadAsync
返回的是只是一个任务,返回时并不保证传入的委托能被调用,所以metadata
变量在方法返回后还是保持 null
。
较好的用法是:
var streamTask = provider.ReadAsync(resource, (o, m) => { Console.WriteLine(m["FileSize"]);});var stream = await streamTask;
总结
两点感悟。
- 多开拓思路。上面说的三个方法各有优缺点,具体选择哪个方法,一要根据实际情况来定,二要遵从一定的设计哲学,灵活也好、严谨也罢,不要太死板。代码是面向人的,时常想想你写的代码如果要给其他人调用,应当怎样设计,使用者才是最得心应手的。
- 看清 .NET 异步方法、lambda 表达式背后的实质,知道在所有这些语法糖后面,编译器都做了些什么,才能完全发挥 C# 这门语言利器的真正威力。
- 在 C# 异步方法中使用 out/ref 参数机制
- C#方法中使用ref和out参数
- C#中Ref/Out参数
- C# Error CS1628: 不能在匿名方法、lambda 表达式或查询表达式中使用 ref 或 out 参数
- C#中方法参数 ref 与 out 的区别
- C#方法参数传递-同时使用ref和out关键字
- C#中out、ref、params参数的使用
- 黑马程序员-在方法中传参数 out ref
- C#的方法参数-Params,Ref,Out
- C#的方法参数--params、ref、out
- c#方法参数ref和out区别
- C#中使用ref和out传递数组的方法
- C# 下ref和out 参数使用
- C# 下ref和out 参数使用
- C# ref和out参数的使用
- c#中方法out参数的使用
- C#中方法的参数四种类型(值参数、ref、out、params)详解
- C#中使用ref和out详解
- 分段和分页内存管理
- HDU 1536 S-Nim
- Android RSA加密解密
- C++学习重点分析
- Android之远程服务器存储
- 在 C# 异步方法中使用 out/ref 参数机制
- 【郑轻】[1900]985的“树”难题
- HDU 1722 Cake
- Android 6.0新特性之Runtime Permission
- ZeroMQ(ZMQ)函数接口英汉直译
- Android——ListView与适配器
- java常用IO流集合用法模板
- 数据结构实验之串三:KMP应用
- 杭电-1875 畅通工程再续(Kruscal)