改善C#编程的50个建议(36-40)

来源:互联网 发布:品质主数据 编辑:程序博客网 时间:2024/06/12 00:46

-------------------------翻译 By Cryking-----------------------------
-----------------------转载请注明出处,谢谢!------------------------ 

36 理解如何使用PLINQ的I/O密集型操作
并行任务库看起来会为CPU密集型操作进行优化。当这个优化成为库的核心任务时,I/O密集型操作也会被优化。事实上,默认情况下并行任务库处理I/O密集型操作已经相当好了。
基于线程的繁忙程序,系统将更新分配给你的算法的线程数。更多的线程阻塞将会导致线程池分配更多的线程给任务。与其他的扩展并行一样,你可以使用方法调用或者LINQ查询语法。
I/O密集型操作的并行与CPU密集型操作的稍微有些不同。比起CPU密集型你通常需要更多的线程,因为I/O密集型线程将花费更多的时间来等待一些外部事件。PLINQ也为此提供了一个框架。
下面的代码执行web下载:
foreach (var url in urls)
{
var result = new WebClient().DownloadData(url);
UseResult(result);
}
DownloadData()调用使得web同步请求和等待直到所有数据被检索。这个算法将花费大量时间来等待。更改为并行处理如下:
Parallel.ForEach(urls, url =>
{
  var result = new WebClient().DownloadData(url);
  UseResult(result);
});
Parallel.ForEach()使得其进入一个并行处理模型。当然这个并行版本的下载会比串行花费更少时间。
你还可以使用PLINQ和查询语法来产生相同的结果:
var results = from url in urls.AsParallel()
select new WebClient().DownloadData(url);
results.ForAll(result => UseResult(result));
PLINQ操作和并行任务库的Parallel.ForEach()支持有一些不同。PLINQ会使用固定数量的线程,而AsParallel()将会调整线程数来增加吞吐量。
在PLINQ中,你可以使用ParallelEnumerable.WithDegreeOfParallelism()来控制线程数,Parallel.ForEach()会帮你管理这些线程。Parallel.ForEach()在加载混合了I/O密集型操作和CPU密集型操作
的时依然会工作的很好。当更多的线程被I/O操作阻塞时,它将创建更多的线程以增加吞吐量。当更多的线程在工作时,它将停止部分活动线程已减少上下文切换。
上面的代码并不是真正的异步处理。并行任务库提供了其他方法来实现异步模式。一个比较常用的模式是:开始是一些I/O密集型任务和在其结果上执行的一些动作。如:
urls.RunAsync(
  url => startDownload(url),
  task => finishDownload(task.AsyncState.ToString(),task.Result));
这里startDownload()方法会下载每个URL,而且每个下载完成时,finishDownload()方法将会被调用。所有下载完成后,RunAsync()也就完成了。
其实最好的开始是RunAsync方法本身:
public static void RunAsync<T, TResult>(this IEnumerable<T> taskParms,
Func<T, Task<TResult>> taskStarter,Action<Task<TResult>> taskFinisher)
{
  taskParms.Select(parm => taskStarter(parm)).
  AsParallel().
  ForAll(t => t.ContinueWith(t2 => taskFinisher(t2)));
}
这个方法对每个输入参数都创建了一个任务。Select()方法返回任务序列。接下来你使用AsParallel()来进行并行处理。对于每一个任务,你将会调用
后置处理方法。Task<T>类表示一个任务并包含执行该任务所获得的输入与输出值报告的属性。ContinueWith()是Task<T>其中的一个方法。
当任务完成时它将被调用,并且在任务完成运行后允许你执行任何处理。在RunAsync方法,它调用taskFinisher,并以Task对象作为参数传递。
ForAll()执行反向枚举,所以它一直被阻塞直到所有任务都完成。
  让我们对这种模式理解更深入一点,检查开始和报告每个下载的进度的方法。finishDownload相当简单:
private static void finishDownload(string url, byte[] bytes)
{
  Console.WriteLine("Read {0} bytes from {1}",bytes.Length, url);
}
而StartDownload方法展示了并行任务库提供的更多接口。使用的特定类型有助于支持Task接口。对于每个你想完成的特定任务,处理这些类型会有一些不同。
实际上,并行任务库在很多不同的异步模式上提供了一个公共的接口,它已存在于.NET BCL以前的版本中:
    private static Task<byte[]> startDownload(string url)
    {
        var tcs = new TaskCompletionSource<byte[]>(url);
        var wc = new WebClient();
        wc.DownloadDataCompleted += (sender, e) =>
        {
            if (e.UserState == tcs)
            {
                if (e.Cancelled)
                    tcs.TrySetCanceled();
                else if (e.Error != null)
                    tcs.TrySetException(e.Error);
                else
                    tcs.TrySetResult(e.Result);
            }
        };
        wc.DownloadDataAsync(new Uri(url), tcs);
        return tcs.Task;
    }
这个方法包含了泛型Task代码和从指定URL下载数据的代码。它首先为任务创建了一个TaskCompletionSource对象。
一个askCompletionSource对象可以使你把创建任务和完成任务分开。它在这里很重要,因为你使用了WebClient类的异步方法来创建任务。
TaskCompletionSource的参数类型也是任务返回的结果类型。
  WebClient类使用了基于事件的异步处理模式(EAP),也就是当一个异常操作完成时你注册的事件处理将会执行。当事件发生时,TaskCompletionSource的startDownload()方法存储
了任务完成信息。TaskSheduler挑选出任务然后下载开始。startDownload方法返回TaskCompletionSource内嵌的任务(return tcs.Task),所以当任务一旦完成,对应的事件结果也就被处理了。
  在这部分工作后,Web下载会在另一线程异步地开始了。一旦下载完成,DownloadDataCompleted事件将会被调用,此时事件处理程序将会设置TaskCompletionSource的完成状态,标志着TaskCompletionSource
嵌入的任务已经完成了。现在,任务将调用报告下载结果的方法ContinueWith().
  这看起来有些绕,但是一旦你弄明白后,会发现这种模式并不难理解。
上面的例子演示了基于事件的异步处理模式。.NET库有些地方使用了异步编程模型(APM).在这种模式,对于一些操作Foo你将调用BeginFoo(),这个方法将返回一个IAsyncResult接口。
一旦操作完成,你将以IAsyncResult做参数调用EndFoo()来结束。在并行任务库,你能使用Task<TResult>.Factory.FromAsync()方法来实现APM模式。
  并行任务库提供了一系列方法来处理I/O密集型操作。使用Task类,你能支持不同的异步模式在I/O密集型操作或I/O和CPU混合密集型操作。并行任务仍然不简单,但是并行任务库和PLINQ
为异步编程提供了更好的语言级支持。


37 构造并行算法时要考虑异常处理
之前的两个例子都没考虑子线程出现错误的情况。这明显不是实际情况。当然,后台线程的异常在几个方面都增加了复杂性。如果一个异常在线程开始时发生,那么这个线程会被终止。调用它的
线程没有办法检索错误或者做任何关于它的事情。更深的,如果你的并行算法必须支持发送问题时的回滚,你将不得不为了理解发生问题带来的任何副作用而做更多的工作,并且你应为从错误中恢复而做些什么。
每个算法都有不同的要求,所以在并行环境中没有一个通用的异常处理方法。
  让我们从上面最后的异步下载方法开始。那是一个没有副作用的简单策略,所有从其他web主机的下载都不关心其中的一个下载是否失败。在并行操作中使用AggregateException类型来处理异常。AggregateException
是一个容器,它将所有从并行操作中产生的异常放在一个InnerExceptions属性里。在这个过程中有两种不同的异常处理方式。首先,我将展示更一般的情况,如果处理外部子任务生成的任何错误。
  上面展示的RunAsync()方法使用了不止一次的并行操作。这说明可能会产生InnerExceptions集合中的AggregateExceptions。并行操作越多,异常处理的嵌套也就越深。因为并行操作方式相互组成,所以在最终的异常集合中你会看到很多
原始异常的副本。修改的RunAsync()方法如下:
        try
        {
            urls.RunAsync(
            url => startDownload(url),
            task => finishDownload(task.AsyncState.ToString(), task.Result));
        }
        catch (AggregateException problems)
        {
            ReportAggregateError(problems);
        }
    private static void ReportAggregateError(AggregateException aggregate)
    {
        foreach (var exception in aggregate.InnerExceptions)
            if (exception is AggregateException)
                ReportAggregateError(exception as AggregateException);
            else
                Console.WriteLine(exception.Message);
    }
ReportAggregateError方法将会打印所有非AggregateException的异常消息。当然,这样做的副作用是吞下了所有异常,无论你是否期待其中某些异常。
这里有足够的递归集合应该采用实用方法, 这个泛型方法必须知道你想处理哪个异常类型,以及哪些不想处理,如何处理等。如:
        try
        {
            urls.RunAsync(
            url => startDownload(url),
            task => finishDownload(task.AsyncState.ToString(), task.Result));
        }
        catch (AggregateException problems)
        {
            var handlers = new Dictionary<Type, Action<Exception>>();
handlers.Add(typeof(WebException),
ex => Console.WriteLine(ex.Message));
if (!HandleAggregateError(problems, handlers))
throw;
        }
HandleAggregateError方法递归查找每个异常。如果异常被捕获,相应的处理程序也就被调用。否则,HandleAggregateError返回false,表示聚合异常集没被正确处理。
    private static bool HandleAggregateError(AggregateException aggregate,
    Dictionary<Type, Action<Exception>> exceptionHandlers)
    {
        foreach (var exception in aggregate.InnerExceptions)
            if (exception is AggregateException)
                return HandleAggregateError(exception as AggregateException,exceptionHandlers);
            else if (exceptionHandlers.ContainsKey(
            exception.GetType()))
            {
                exceptionHandlers[exception.GetType()]
                (exception);
            }
            else
                return false;
        return true;
    }
当HandleAggregateError遇到一个AggregateException,它会进行递归调用,遇到其他异常,它会先查看字典,如果Action<>处理程序已经注册,它就会调用该
处理程序,否则立即返回false,表示发现了一个不应处理的异常。
  你可能会觉得奇怪,为什么不是没被处理的单个异常抛出而是原始的AggregateException.原因是从集合抛出单个的异常会丢失重要信息。InnerExceptions可能包含
任何数量的异常,其中有些类型可能是不需要的。很多情况下,AggregateException的InnerExceptions集合只有一个异常。但是尽管如此,你不能当作只有一个异常的方式来编码。
  
38 理解动态类型的正反两面
C#支持动态类型,但并不意味着鼓励一般的动态语言编程,而是为了一个在C#相关的强类型、静态类型和使用动态类型模型的环境之间提供平滑转换。
尽管如此,也不是说限制你使用的动态类型和其他环境交互。C#类型能被强制转为动态类型。就像任何事一样,把C#对象当作动态对象有好也有坏。       
public static dynamic Add(dynamic left,dynamic right)
{
  return left + right;
}
dynamic可以被视为运行时绑定的System.Object.在编译时,动态变量只有System.Object中已定义的方法。在运行时,执行代码会检查对象并且
决定请求的方法是否可用。这个通常被称为"鸭子类型(duck typing)":如果它走路和说话都像只鸭子,它可能就是一只鸭子。你不需要声明一个特定的接口或提供
任何编译时类型操作。只要成员在运行时被需要是可用的,那么它就可用了。
  上面的方法,动态调用点决定了实际运行时类型是否有一个可访问的+操作。
dynamic answer = Add(5, 5);
answer = Add(5.5, 7.3);
answer = Add(5, 12.3);
注意answer也必须声明为dynamic类型。因为调用是动态的,编译器不知道返回值的类型。这些都必须在运行时来确定。当然这个动态的Add方法不限于数值类型.
你也可以对string相加.
dynamic label = Add("Here is ", "a label");
也可以对时间类型进行相加:dynamic tomorrow = Add(DateTime.Now, TimeSpan.FromDays(1));
这种开放式的动态方法可能导致你过度地使用动态编程。这还只是在讨论动态编程的正面,现在我们来讨论其不好的一面。
你脱离了系统后的类型安全,而且,你限制了编译器对你的帮助。任何动态类型的错误只能在运行时才能被发现。有时候,想将返回的动态对象转换
为静态对象,你需要一个CAST或CONVERT操作。
answer = Add(5, 12.3);
int value = (int)answer;
string stringLabel = System.Convert.ToString(answer);
当返回的动态类型实际是目标类型或能转换为目标类型时,上面的代码能正常工作。否则在运行时转换将会失败,并抛出异常。
  当你不得不在运行时来解决方法而又没有所涉及的类型的相关知识时,使用动态类型是正确的。当你有了编译时的认识,你应该使用
lambda表达式和函数式编程结构来创建你需要的解决方案。使用Lambda表达式重写上面的Add方法如下:
public static TResult Add<T1, T2, TResult>(T1 left, T2 right,Func<T1, T2, TResult> AddMethod)
{
  return AddMethod(left, right);
}
每个调用者将要求提供具体的方法:
var lambdaAnswer = Add(5, 5, (a, b) => a + b);
var lambdaAnswer2 = Add(5.5, 7.3, (a, b) => a + b);
var lambdaAnswer3 = Add(5, 12.3, (a, b) => a + b);
var lambdaLabel = Add("Here is ", "a label",(a, b) => a + b);
dynamic tomorrow = Add(DateTime.Now, TimeSpan.FromDays(1));
var finalLabel = Add("something", 3,(a,b) => a + b.ToString());
你可以看到最后的方法需要你指定显示的int到string转换.上面那些所有的lambda表达式感觉有些丑陋,看起来可以转化为一个共同的方法。
不幸的是,这只是这个解决方案是如何工作的.你不得不在推断类型的地方提供lambda表达式,那么会有大量看起来类似的代码必须重复出现。
当然,定义这个Add方法来实现相加看起来是愚蠢的。在实际中,你为方法使用lambd表达式这种技术而不应是简单地执行它..NET库的Enumerable.Aggregate()
使用了这种技术。Aggregate()枚举所有的序列并且通过相加产生一个结果。
var accumulatedTotal = Enumerable.Aggregate(sequence,(a, b) => a + b);
不然这看起来仍然是重复的代码。避免这种重复代码的一种方式是使用表达式树.这是另一种在运行时构建代码的方式。
System.Linq.Expression类和它的派生类都支持建立表达式树。一旦你建立表达式树,你把它转换为lambda表达式并且编译生成的lambda表达式结果为一个委托。
如:
   // Naive Implementation. Read on for a better version
    public static T AddExpression<T>(T left, T right)
    {
        ParameterExpression leftOperand = Expression.Parameter(typeof(T), "left");
        ParameterExpression rightOperand = Expression.Parameter(typeof(T), "right");
        BinaryExpression body = Expression.Add(leftOperand, rightOperand);
        Expression<Func<T, T, T>> adder =
        Expression.Lambda<Func<T, T, T>>(
        body, leftOperand, rightOperand);
        Func<T, T, T> theDelegate = adder.Compile();
        return theDelegate(left, right);
    }
大多数有趣的工作都包含类型信息,因此与其使用var倒不如在生产代码清晰地指定所有的类型。开始的两行为变量"left"和"right"创建了参数表达式。
接下来一行使用上面创建的两个参数创建了Add表达式。Add表达式派生于BinaryExpression.你也可以为其他二进制操作创建相似的表达式。
接下来,你需要从表达式体和那两个参数来创建lambda表达式。最终,你通过编译表达式创建了Func<T,T,T>委托。一旦编译,你就能执行它并且返回结果。
当然,你也可以像其他一般方法一样调用它:
int sum = AddExpression(5, 7);
注意请不要在你的工作应用里使用上面的代码。因为它有两个问题。一是:有很多Add()应该工作的情况实际它不会工作,如:当Add()方法的参数为int和double,DateTime和TimeSpan等时,它无法工作.
修复这个问题如下:
    // A little better.
    public static TResult AddExpression<T1, T2, TResult>(T1 left, T2 right)
    {
        var leftOperand = Expression.Parameter(typeof(T1),
        "left");
        var rightOperand = Expression.Parameter(typeof(T2),
        "right");
        var body = Expression.Add(leftOperand, rightOperand);
        var adder = Expression.Lambda<Func<T1, T2, TResult>>(
        body, leftOperand, rightOperand);
        return adder.Compile()(left, right);
    }
这个和上面的代码很相似。它只是使得你可以使用不同类型来作为左和右操作数。不好的是当你调用这个版本的方法时你需要指定三个参数类型。
如:
int sum2 = AddExpression<int, int, int>(5, 7);
但是因为你指定了三个参数的类型,所以表达式现在可以使用不同的参数来工作:
DateTime nextWeek= AddExpression<DateTime, TimeSpan,DateTime>(DateTime.Now, TimeSpan.FromDays(7));
上面展示的所有代码,每当AddExpression()方法被调用时,每次都会编译表达式到一个委托。这是非常没效率的,特别是你最终都是重复地执行相同的
表达式的时候。表达式编译是耗时的,所以你应缓存这个委托编译以备将来的调用。
如:
        //危险但是可以工作的版本
        public static class BinaryOperator<T1, T2, TResult>
        {
            static Func<T1, T2, TResult> compiledExpression;
            public static TResult Add(T1 left, T2 right)
            {
                if (compiledExpression == null)
                    createFunc();
                return compiledExpression(left, right);
            }
            private static void createFunc()
            {
                var leftOperand = Expression.Parameter(typeof(T1),"left");
                var rightOperand = Expression.Parameter(typeof(T2),"right");
                var body = Expression.Add(leftOperand, rightOperand);
                var adder = Expression.Lambda<Func<T1, T2, TResult>>(
                body, leftOperand, rightOperand);
                compiledExpression = adder.Compile();
            }
        }
此时,你可能想哪种技术应该使用:动态还是表达式? 结果取决于具体情况。表达式版本使用了稍微简单的运行时操作集合,这在很多环境下可能会运行的更快。
但是表达式不够动态。动态版本可以使用不同的类型参数:int和double,short和float等等。甚至可以是一个string和number相加。如果你在这些情况下使用表达式
版本,你可能会获得InvalidOperationException错误。即使有做转换操作,表达式版本也不会把转换操作带到lambda表达式里。动态调用可以做更多的事情,因此也支持
更多不同类型的操作。例如,假设你想更新AddExpression来支持不同类型并且支持适当的转换,你需要更新代码来建立包含转换操作的表达式:
        // A fix for one problem causes another
        public static TResult AddExpressionWithConversion
        <T1, T2, TResult>(T1 left, T2 right)
        {
            var leftOperand = Expression.Parameter(typeof(T1),"left");
            Expression convertedLeft = leftOperand;
            if (typeof(T1) != typeof(TResult))
            {
                convertedLeft = Expression.Convert(leftOperand,
                typeof(TResult));
            }
            var rightOperand = Expression.Parameter(typeof(T2),"right");
            Expression convertedRight = rightOperand;
            if (typeof(T2) != typeof(TResult))
            {
                convertedRight = Expression.Convert(rightOperand,
                typeof(TResult));
            }
            var body = Expression.Add(convertedLeft, convertedRight);
            var adder = Expression.Lambda<Func<T1, T2, TResult>>(
            body, leftOperand, rightOperand);
            return adder.Compile()(left, right);
        }
现在支持了需要类型转换的相加,如double和int的相加,double和string的相加并以string返回。尽管如此,但它破坏了一些有效的使用,因为参数类型和结果类型不一样了。
尤其是它也不支持TimeSpan和DateTime的相加,可能添加更多代码,你能解决这个问题。此时,使用动态类型吧。
当操作数和结果都一样类型的时候,你应使用表达式版本,因为这样可以给你一个泛型参数类型接口以及当代码在运行时失败后做更少的转换操作。
下面这个版本是建议在实现运行时调度的表达式版本:
      public static class BinaryOperators<T>
        {
            static Func<T, T, T> compiledExpression;
            public static T Add(T left, T right)
            {
                if (compiledExpression == null)
                    createFunc();
                return compiledExpression(left, right);
            }
            private static void createFunc()
            {
                var leftOperand = Expression.Parameter(typeof(T),"left");
                var rightOperand = Expression.Parameter(typeof(T),"right");
                var body = Expression.Add(leftOperand, rightOperand);
                var adder = Expression.Lambda<Func<T, T, T>>(
                body, leftOperand, rightOperand);
                compiledExpression = adder.Compile();
            }
        }
当你调用这个Add时,你仍需要指定一个类型参数.这样做的目的是可以利用编译器来在调用点创建任何的类型转换。编译器可以提示int到double类型等。        
在运行时建立表达式和使用动态都有性能开销。就像动态类型系统,你的程序需要在运行时做更多的工作来检查类型。在大多数情况下,使用动态类型将比
使用反射来生成后期绑定的个人版本快的多。但是不用怀疑的是,如果你能使用静态类型解决的,一定会比使用动态类型的要高效。
  当你控制了所有相关的类型,你可以创建一个接口来代替动态编程,而且这是一个更好的方案。你可以定义接口,使用该接口编程,在所有你自己的类型中实现
该接口的相关行为。这样在你的代码中C#类型系统将很难产生错误,并且编译器会生成更高效的代码,因为它能假定类的某些错误是不可能出现的。
  很多情况下,你能使用lambda来创建泛型API并且强制调用者去定义你将要在动态算法中执行的代码。
  如果你有一些小数量的不同类型的转换和小数量的可能的转换,你应选择使用表达式。你可以控制什么表达式将创建,以及在运行时做多少工作。
当你使用动态,下面的动态架构将会做任何可能的合法构建工作,而不管这些工作在运行时多么的耗费时间。
  建立基于存在一个特定成员方法的最好的方式是写一个推迟那些选择到运行时的动态方法。动态实现将会发现一个合适的实现,使用它,为了更好的性能缓存它,
而且这比解析表达式树要更简单。


39 使用动态来影响泛型类型参数的运行时类型
  System.Linq.Enumerable.Cast<T>强制转换序列中的每个类型到目标类型T.它是框架的一部分,以至于LINQ查询能被枚举器(IEnumerable)使用。Cast<T>是一个泛型方法,
它有不带约束的T。这限制了可用的类型转换来使用它。如果你使用Cast<T>时忘记了它的这个局限性,你将发现它有时不能工作。但实际上,这个方法像它应该工作的那样,而
不是你所想的那样。让我们来检查它的内部工作和局限性。然后,你将会轻易地创建你想要的各种不同的版本。
  根本问题是事实上Cast<T>被编译成MSIL时不知道任何关于T的东西。(其实T必须是一个派生于System.Object的托管类型)因此,它只能使用定义在System.Object中的功能。
检查下面这个类:
   public class MyType
    {
        public String StringMember { get; set; }
        public static implicit operator String(MyType aString)
        {
            return aString.StringMember;
        }
        public static implicit operator MyType(String aString)
        {
            return new MyType { StringMember = aString };
        }
    }
看第28条建议我们可以知道为什么转换操作是不好的。但尽管如此,一个用户自定义转换操作是这个问题的关键。考虑下面的代码:
        var answer1 = GetSomeStrings().Cast<MyType>();
        try
        {
            foreach (var v in answer1)
                Console.WriteLine(v);
        }
        catch (InvalidCastException)
        {
            Console.WriteLine("Cast Failed!");
        }
在开始这个问题前,你可能期望GetSomeStrings().Cast<MyType>()将使用MyType定义的隐式转换操作来正确地转换每个string到MyType.
但现在你知道这是不可能的,它将抛出一个InvalidCastException异常。
  上面的代码等价于下面使用查询表达式的代码:
        var answer2 = from MyType v in GetSomeStrings()
                      select v;
        try
        {
            foreach (var v in answer2)
                Console.WriteLine(v);
        }
        catch (InvalidCastException)
        {
            Console.WriteLine("Cast failed again");
        }
范围变量的类型声明被转换为由编译器调用Cast<MyType>.但是,它还是会抛出InvalidCastException异常.
重建该代码如下以使它能工作:
var answer3 = from v in GetSomeStrings()
select (MyType)v;
foreach (var v in answer3)
  Console.WriteLine(v); 
有什么不同?那两个不能工作的版本都使用了Cast<T>(),而这个工作的版本包含转换在lambda中,做为了select()的参数.
Cast<T>不能访问任何基于运行时类型参数的用户自定义转换。它可以使用的唯一转换是引用转换和装箱转换。当is操作成功时,一个引用
转换也会成功(参考建议3).装箱转换将一个值类型转换为引用类型,拆箱相反。Cast<T>不能访问任何用户自定义转换,因为它只能假定T包含了
定义在System.Object的成员。System.Object没有包含任何用户自定义转换,所以这些也不合适。使用了Select<T>的版本成功了是因为lambda使用
了带string类型输入参数的Select().
  正如我以前指出的,我通常视转换操作为一个奇怪的代码。有时转换操作可能会很有用,但大多数情况下它会制造更多的问题。这里,如果没有转换操作,没有
开发者将会尝试去写那些不能工作的代码。
  当然,如果我不建议使用转换操作,我应提供另一个选择。MyType已经包含了一个读写属性来存储string属性,所以你能移除转换操作并写下面这些操作:
var answer4 = GetSomeStrings().
Select(n => new MyType { StringMember = n });
var answer5 = from v in GetSomeStrings()
select new MyType { StringMember = v };
如果你需要,你也能为MyType创建不同的构造函数。当然,这些都带有Cast<T>()的局限性。现在你理解了为什么会存在这些局限性,是时候去写一个绕开该局限性的方法了。
技巧是利用运行时信息来执行任何的转换操作。
  你可能会整页整页地使用基于反射的代码来查看哪些转换是可用的,然后执行这些转换,再返回合适的类型。你可以这样做,但这非常费时。C# 4.0动态特性可以使这些操作变得其更轻便。
如下你只要做一个简单的Convert<T>便可实现你期望的转换:
        public static IEnumerable<TResult> Convert<TResult>(this System.Collections.IEnumerable sequence)
        {
            foreach (object item in sequence)
            {
                dynamic coercion = (dynamic)item;
                yield return (TResult)coercion;
            }
        }
现在,只要有一个从源类型到目标类型转换(无论是显式或者隐式),它都会工作。这里仍牵涉转换,所以运行时的失败也仍可能存在.Convert<T>比Cast<T>()适应更多方案,但它仍做了过多的工作。
作为开发者,我们应更多关心用户需要去创建的代码而不仅仅是我们自己的代码。Convert<T>通过了下面的测试:
var convertedSequence = GetSomeStrings().Convert<MyType>();
Cast<T>像所有泛型方法,只限于已知的参数类型的编译,这会导致泛型方法不按您期望的方式去工作。根本的原因几乎总是泛型方法不能意识到类型参数中的某些类型代表的特定功能.当其发生了,
一个小的使用运行时反射的动态应用可以使工作变得正确。


40 使用接收匿名类型的动态参数
匿名类型的一个缺点是你不能在方法的参数或返回类型中容易地书写它们。因为编译生成匿名类型后,你不能简单地将它们作为方法的参数或者返回类型。针对这个问题的任何解决方案都
有一定的限制。你可能将匿名类型作为泛型类型参数,或者使用在带System.Object类型的参数的方法中。这些都不会感到特别满意。泛型方法假定了功能以System.Object定义.System.Object
也有同样的限制。当然,有些时候,你将发现你真的需要一个带实际行为的命名类。这一节讨论当你想使用不同匿名类型在相同属性名称时怎么做,但这不是你的核心应用程序的一部分并且不保证
会创建一个新的命名类型。
  动态的静态类型使你克服这个限制。动态使得运行时绑定并命令编译器在必要的时候生成任何可能的运行时类型。
  假设你需要打印一个价目表信息。再假设你的价目表可能来自不同的数据源.你可以有一个库存的数据库,订单库,还有一个通过第三方供应商的销售库.因为这些都是完全不同的系统,它们可能都
有对产品的不同抽象。这些不同的抽象可能没有相同的属性名称,他们也当然不会有同样的基类或实现同样的接口。针对这个问题的经典答案是为每个抽象产品实现适配器模式,转换每个对象为一个类型。
这个工作量是非常大的,每次增加一个新的产品抽象都需要做非常多的工作.尽管如此,适配器模式仍使用在静态类型系统,并且会有更好的性能。
  另外,轻量级的方案是使用动态来创建一个可以使用任何类型的方法帮助你寻找价格信息:
        public static void WritePricingInformation(dynamic product)
        {
            Console.WriteLine("The price of one {0} is {1}",product.Name, product.Price);
        }
你可以创建一个匿名类型来完成你想要查询出的价格信息:
            var price = from n in Inventory
                        where n.Cost > 20
                        select new { n.Name, Price = n.Cost * 1.15M };
你可以使用任何映射包含动态方法中所有的必须属性来创建一个匿名类型。只要你有了"Price"和"Name"属性,WritePricingInformation方法将会工作。
  当然,你也可以使用带其他属性的匿名类型。
            var orderInfo = from n in Ordered
                            select new
                            {
                                n.Name,
                                Price = n.Cost * 1.15M,
                                ShippingCost = n.Cost / 10M
                            };
动态使用的地方,旧的C#对象也能被使用.意思是当使用具体类型和正确的属性时,价格信息方法也能被正常使用:
        public class DiscountProduct
        {
            public static int NumberInInventory { get; set; }
            public double Price { get; set; }
            public string Name { get; set; }
            public string ReasonForDiscount { get; set; }
            // other methods elided
        }
你可能注意到Price属性的类型是double,而在匿名类型中是decimal.这是可以的.WritePricingInformation方法使用的是动态的静态类型.
它会在运行时指出正确的类型.当然,如果DiscountProduct是从一个基类Product派生的话,Product类包含了Name和Price属性,这也是可以的。
  上面的代码很容易让你们相信我是提倡动态的,其实不然。动态调用是解决这类问题的一种比较好的方式,但是不要过度使用它。动态调用意味
着有额外的开销。在必要的时候这个开销是值得的,但当你能避免这个开销的时候,你应该避免。
  你可以创建WritePricingInformation()方法的重载来针对你的对象模型中的Product类。
        public class Product
        {
            public decimal Cost { get; set; }
            public string Name { get; set; }
            public decimal Price
            {
                get { return Cost * 1.15M; }
            }
        }


        // Derived Product class:
        public class SpecialProduct : Product
        {
            public string ReasonOnSpecial { get; set; }
            // other methods elided
        }


        // elsewhere
        public static void WritePricingInformation(dynamic product)
        {
            Console.WriteLine("The price of one {0} is {1}",
            product.Name, product.Price);
        }
        public static void WritePricingInformation(Product product)
        {
            Console.WriteLine("In type safe version");
            Console.WriteLine("The price of one {0} is {1}",
            product.Name, product.Price);
        }
对于任何的Product类型或其派生类型编译器会自动选择指定的版本来调用。对于其他的,编译器将使用静态类型来动态版本,这包括匿名类型。内在的,动态绑定器将为它使用的每个方法缓存
方法信息,这将为经常调用带同样匿名类型参数的WritePricingInformation()减少开销。一旦在第一次调用时方法绑定执行了,它将会在随后的每次调用中重复使用。它虽然也有开销,但动态
实现做了更多可能的工作来减少使用动态带来的开销。
  不允许创建动态对象的扩展方法.你可以利用动态来创建使用匿名类型的方法。这种技术使用得比较少,如果你发现你自己使用动态调用创建了很多带匿名类型的方法,这会是一个重大缺陷,
你应创建一个具体类型来实现.随着时间推移,它会比动态更容易维护.尽管如此,当你需要创建一或两个使用匿名方法的工具,动态调用将是一种简单的方法。
  
0 0
原创粉丝点击