异步辅助类






4.97/5 (17投票s)
这是一个通用类,可以大大简化在后台创建另一个类。它还提供了一个如何使用带 ContinueWith 子句的 Task.Run 的好示例。
引言
我曾经有一个场景,需要将另一个开发人员正在处理的一个类放到后台线程上创建,因为它启动应用程序花费的时间太长。我最初实现了一个非泛型版本,但后来决定使用一个测试项目创建了一个泛型实现,该项目还提供了一种更好地测试该概念是否有效的方法。这是幸运的,因为我发现了一个在真实应用程序中会更难发现的 bug,而不是在测试项目中。
背景
添加到 C# 的 async
和 await
关键字在正确的情况下(如处理事件)非常有用,但不幸的是,对于实例化类来说它们并不太有用。我似乎花了大量时间试图加快类的实例化速度,因为启动应用程序或初始化类(不使用事件)通常涉及大量处理。将新窗口的启动放在类本身中,而不是放在 event
处理程序中,通常也更方便。
设计
async
辅助类的代码如下:
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace AsyncHelperSample
{
public class AsyncWrapper<T> where T : class
{
private T _item;
private Task loadTask;
private double _initializationMilliseconds;
public T Item
{
get
{
if (_item == null)
loadTask.Wait();
return _item;
}
set { _item = value; }
}
public double InitializationMilliseconds
{
get
{
if (_item == null)
loadTask.Wait();
return _initializationMilliseconds;
}
}
internal void AbstractLoadAsync(Action completeInitialization = null)
{
var startTime = DateTime.Now;
var constructor = typeof(T).GetConstructors().FirstOrDefault
(c => c.GetParameters().Length == 0);
Debug.Assert(constructor != null,
"The AbstractLoadAsync constructor without the constructor parameter requires "
+ "a parameterless constructor");
loadTask = Task.Run<T>(() =>
{
var item = constructor.Invoke(new object[] { });
return (T) item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
internal void AbstractLoadAsync(Func<T> constructor, Action completeInitialization = null)
{
var startTime = DateTime.Now;
loadTask = Task.Run<T>(() =>
{
var item = constructor();
return (T)item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
public void ExecuteAsync(Action<T> set)
{
if (_item == null)
Task.Run(() => loadTask.Wait())
.ContinueWith(result => set(Item));
else
set(Item);
}
}
}
该类有两个构造函数:一个不接受 Func<T>
类型参数的构造函数,以及一个接受该参数的构造函数。
没有构造函数参数的 AsyncWrapper
构造函数要求该类有一个默认构造函数。使用反射来查找默认构造函数,如果找不到,将抛出异常。在调试模式下,如果没有默认构造函数,将显示一个警告。使用类的默认构造函数的缺点是无法在初始化之前向类提供初始化信息。这可能不是一个严重的问题,因为我们可以使用服务来获取类所需的任何数据,而 Action<CompletedEventArgs<T>>
参数允许对初始化后可以更改的任何值进行操作。使用此构造函数的优点是它更直接。
internal void AbstractLoadAsync(Action completeInitialization = null)
{
var startTime = DateTime.Now;
var constructor = typeof(T).GetConstructors().FirstOrDefault
(c => c.GetParameters().Length == 0);
Debug.Assert(constructor != null,
"The AbstractLoadAsync constructor without the constructor parameter requires "
+ "a parameterless constructor");
loadTask = Task.Run<T>(() =>
{
var item = constructor.Invoke(new object[] { });
return (T) item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
此构造函数的使用非常直接
public SecondaryViewModelAsyncWrapper
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
这是派生自 AsyncWrapper
class
的类中的构造函数,它有一个参数,允许实例化类的代码在包装类初始化后执行一个代码,并且还订阅了 SecondaryViewModel
中的单个事件,以便初始化该类的代码可以通过简单地订阅包装 class
中的 event
来订阅被包装 class
中的 event
。这样,初始化代码就不必等待被包装 class
初始化。在调用包装 class
时,使用 completeInitialization
参数是可选的。
对于带构造函数参数的 AsyncWrapper
构造函数,还有一个额外的复杂性,即提供一个返回类实例的 Func<T>
。
internal void AbstractLoadAsync(Func<T> constructor, Action completeInitialization = null)
{
var startTime = DateTime.Now;
loadTask = Task.Run<T>(() =>
{
var item = constructor();
return (T)item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
此构造函数的使用稍微不那么直接
var asyncViewModel = new SecondaryViewModelAsyncWrapper2(
() => new SecondaryViewModel("Scott"),
item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
我最初将 Action<CompletedEventArgs<T>>
作为参数的一个原因是因为派生类会复制正在异步创建的类中的任何事件。这些事件可以在 AsyncWrapper
class
中订阅,而无需在创建 AsyncWrapper
的类之后在原始 class
中订阅这些事件。它还允许执行任何其他初始化,例如提供类的父对象的指针。
此操作的另一个原因是让使用 AsyncWrapper
的类在初始化完成后得到通知。
我还有一个可能不需要但可能有用的额外功能,它可以在初始化类后异步地更改类中的内容。
public void ExecuteAsync(Action<T> set)
{
if (_item == null)
Task.Run(() => loadTask.Wait())
.ContinueWith(result => set(Item));
else
set(Item);
}
此方法允许定义一个将在类初始化完成后执行的 Action
,但可以随时定义。如果类已初始化,则 Action
将立即执行,否则它将等待,不会暂停线程,并在另一个线程上等待初始化完成。
创建派生类
要使用 AsyncWrapper
abstract
class
,必须创建一个继承自此通用 class
的类,其中泛型 Type
是被包装的类。在最初的设计中,我设想被包装类的每个事件都将在包装器中实现,但这可以省略,因为所有需要的是在 AsyncWapper
构造函数的 Action
参数中订阅一个 event
,该参数在初始化完成后执行。如果被包装类有默认构造函数,那么最简单的实现如下:
class SecondaryViewModelAsyncWrapper1 : AsyncWrapper<SecondaryViewModel>
{
public SecondaryViewModelAsyncWrapper1
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
completedAction?.Invoke(this);
});
}
}
这是示例中的实现
class SecondaryViewModelAsyncWrapper1 : AsyncWrapper<SecondaryViewModel>
{
public event EventHandler<string> SendEvent;
public SecondaryViewModelAsyncWrapper1
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
private void SendEventHandler(object sender, string e)
{
SendEvent?.Invoke(sender, e);
}
}
我可以看到两种实现都有其合理性,但我认为第一种更好,并且保留了旧的概念是因为有些人可能认为将每个 event
添加到包装器是首选。也可能还有其他原因。
当可以使用默认构造函数时,EventWrapper
派生类创建如下:
var asyncViewModel = new SecondaryViewModelAsyncWrapper1(item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
在这种情况下,可以看到被包装类被赋值给 SecondaryViewModel
——这会导致 PropertyChanged
event
的触发,从而通知 View
SecondaryViewModel
属性已更改。通常,此 AsyncWrapper
会与 Model
一起使用,而不是与 ViewModel
一起使用,当 Model
初始化完成后,ViewModel
的属性将被更新,并触发 PropertyChange
event
。
如果 class
需要带参数的构造函数(这是通常情况),则包装 class
的用法如下:
class SecondaryViewModelAsyncWrapper2 : AsyncWrapper<SecondaryViewModel>
{
public SecondaryViewModelAsyncWrapper2(Func<SecondaryViewModel> constructor,
Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(
() =>
{
return constructor();
},
() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
}
包装 class
的构造函数需要包含被包装 class
构造函数所需的所有参数,外加一个参数用于在初始化完成后执行的 Action
。
当不使用默认构造函数时,EventWrapper
派生 class
创建如下:
var asyncViewModel = new SecondaryViewModelAsyncWrapper2(
() => new SecondaryViewModel("Scott"),
item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
示例
示例中有两个按钮——一个使用默认构造函数异步创建 class
,另一个使用带 string
参数的构造函数创建 class
。当按下 Button
时,class
的实例化开始,控件将被灰色化。为了在实例化过程中产生延迟,构造函数中有一个 5 秒的 sleep。当 class
被实例化后,控件不再灰色化,TextBox
显示默认名称。在默认构造函数的情况下,此值使用 ExecuteAsync
方法更新。对于带 string
参数的构造函数,该值是传递给构造函数的 string
值。此外,实例化 class
所需的时间显示在 TextBox
下方。
结论
相同的代码可用于允许在内部实例化一个类,但使用这个单独的 class
消除了需要订阅一个事件来指示实例化何时完成的需要。
历史
- 2017/08/23:初始版本