泛型、委托和事件

​ 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能。基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用;同时,它减少了泛型类以及泛型方法中的转型,确保了类型的安全。委托本身是一种引用类型,它保存的是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用;

​ 事件本身也是委托,它是委托组,C#中提供了关键字event来加以区别。一旦我们开始编写稍微复杂的C#代码,就肯定离不开泛型/委托和事件。我们需要对这三个方面分别进行说明;

建议32 总是优先考虑泛型

​ 泛型的优点是多方面的,无论是泛型类还是泛型方法都同时具备可重用性,类型安全和高效率等特性,这些都是非泛型类和非泛型方法无法具备的;这条建议将从可重用性类型安全高效率三个方面来进行剖析在实际的编码过程中为何总是应该优先考虑泛型。

(1) 可重用性, 比如简单的设计一个集合类,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MyList
{
    int[] items;
    public int this[int i]
    {
        get { return items[i]; }
        set { this.items[i] = value; }
    }

    public int Count
    {
    	get { return items.Length; }
    }

	////省略一些其他方法
}

​ 该类型只支持整型,如果要让类型支持字符串,有一种方法是重新设计一个类。但是这两个类型的属性和方法都是非常接近的,如果有一种方法可以让类型接收一个通用的数据类型,这样就可以进行代码复用了,同时类型也只要一个就够了。泛型完成的就是这样的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class MyList<T>
{
    T[] items;

    public T this[int i]
    {
        get { return items[i]; }
        set { this.items[i] = value; }
    }

    public int Count
    {
    	get { return items.Length; }
    }

    ///省略其他方法
}

​ 可以把T理解成一个占位符,那么在C#泛型编译生成的IL代码中,T就是一个占位符的角色。在运行时,即时编译器(JIT)会用实际代码中的输入的T类型来代替T,也就是说,在由JIT生成的本地代码中,已经使用了实际的数据类型。我们可以把MyList<int>MyList<string>视作是两个完全不同的类型,但是,这仅是对本地代码而言的,对于实际的C#代码,它仅仅是拥有一个类型,那就是泛型类型MyList<T>

​ 以上从代码重用性的角度来论证了泛型的优点。继续从类型MyList<T>的角度论述,如果不用泛型实现代码重用,另一种方法是让MyList的编码从object的角度去设计。在C#的世界中,所有类型(包括值类型和引用类型)都是继承自object,如果要让MyList足够通用,就需要让MyList针对object编码,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MyList
{
	object[] items;
    public object this[int i]
    {
        get { return items[i]; }
        set { this.items[i] = value; }
    }

    public int Count
    {
    	get { return items.Length; }
    }

	////省略一些其他方法
}

这会让以下代码编译通过:

1
2
3
MyList list = new MyList();
list[0] = 123;
list[1] = "123";

由上面两行代码带来的问题就是非”类型安全性“。该问题实际在建议20 中已经详细论述过了。让类型支持类型安全,可以让程序在编译期间就过滤掉部分Bug,同时也能让代码规避掉”转型为object类型“或“从object转型为实际类型”所带来的效率损耗。尤其是涉及的操作类型是值类型时,还会带来装箱和拆箱的性能损耗。

例如,上文代码中的: list[1] = "123"; 因为它首先转型为object, 继而存储到items这个object数组中去了;

​ 泛型为C#带来的是革命性的变化,FCL之后的很多功能都是借助泛型才得到了很好的实现,如LINQ。LINQ借助于泛型和扩展方法,有效地丰富了集合的查询功能,同时避免了代码爆炸并提升了操作的性能。我们在设计自己的类型时,应充分考虑到泛型的优点,让自己的类型成为泛型类。

建议33 避免在泛型类型中声明静态成员

​ 在上一个建议中,已经解释了应该将MyList<int>MyList<string> 视作两个完全不同的类型,所以,不应将MyList<T>中的静态成员理解成为MyList<int>MyList<string>共有的成员。

​ 对于一个非泛型类型,以下的代码很好理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class MyList
{
	public static int Count { get; set; }

    public MyList()
    {
    	Count++;
    }
}

class Program
{
    static void Main(string[] args)
    {
        MyList myList1 = new MyList();
        MyList mylist2 = new MyList();
        Console.WriteLine(MyList.Count);
        Console.ReadLine();
    }
}

结果返回为2. 如果将MyList换成泛型类型,看看下面的代码会输出什么呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;

namespace Advice33
{
    public class MyList<T>
    {
        public static int Count { get; set; }

        public MyList()
        {
            Count++;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            MyList<int> myList1 = new MyList<int>();
            MyList<int> myList2 = new MyList<int>();

            MyList<string> myList3 = new MyList<string>();

            Console.WriteLine(MyList<int>.Count);
            Console.WriteLine(MyList<string>.Count);
            Console.ReadLine();
        }
    }
}

代码输出为:

1
2
2
1

实际上,随着你为T指定不同的数据类型,MyList<T>相应的也变成了不同的数据类型,在它们之间是不共享静态成员的。

不过,从上文我们也觉察到了,若T所指定的数据类型是一致的,那么两个泛型对象间还是可以共享静态成员的,如上文的myList1myList2。但是,为了规避因此而引起的混淆,仍旧建议在实际的编码工作中,尽量避免声明泛型类型的静态成员。

上面举的例子是基于泛型类型的,非泛型类型中静态泛型方法看起来很接近该例子,但是应该始终这样来理解:

非泛型类型中的泛型方法并不会在运行时的本地代码中生成不同的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;

namespace Advice33
{
    public class MyList
    {
        public static int Count { get; set; }

        public static int Func<T>()
        {
            return Count++;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(MyList.Func<int>());
            Console.WriteLine(MyList.Func<int>());
            Console.WriteLine(MyList.Func<string>());

            Console.ReadLine();
        }
    }
}

代码的输出结果是:

1
2
3
0
1
2

建议34 为泛型参数设定约束

“约束"这个词可能会引起歧义,有些人可能认为对泛型参数设定约束是限制参数的使用,实际情况正好相反。没有“约束”的泛型参数作用很有限,倒是“约束”让泛型参数具有了更多的行为和属性。

查看下面代码,我们会发现Compare方法的参数t1或参数t2仅仅具有object的属性和行为,所以几乎不能在方法中对它们进行任何的操作:

我们尝试在泛型参数上设定约束,会发现,在加了约束之后,我们会发现参数t1或者参数t2会变成一个有用的对象。由于为其指定了对应的类型,t1t2就是一个Salary类型了,在方法的内部,它拥有了BaseSalaryBonnus,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
namespace Advice34
{
    public class Program
    {
        static void Main(string[] args)
        {

        }
    }

    public class Salary
    {
        public string Name { get; set; }

        public int BaseSalary { get; set; }

        public int Bonus { get; set; }
    }

    public class SalaryComputer
    {
        public int Compare<T>(T t1, T t2) where T : Salary
        {
            if (t1.BaseSalary > t2.BaseSalary)
            {
                return 1;
            }
            else if (t1.BaseSalary == t2.BaseSalary)
            {
                return 0;
            }
            else
            {
                return -1;
            }
        } 
    }
}

那么可以为泛型参数指定哪些参数呢?

  1. 指定参数是值类型(除Nullable外),可以有如下几种形式:

    1
    2
    3
    4
    
    public void Method1<T>(T t) where T : struct
    {
    
    }
    
  2. 指定参数是引用类型,可以有如下形式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public void Method1<T>(T t) where T : class
    {
    
    }
    
    public void Method1<T>(T t) where T : Salary
    {
    
    }
    

    注意object不能用来作为约束;

  3. 指定参数具有无参数的公共构造函数,可以有如下形式:

    1
    2
    3
    4
    
     public void Method2<T>(T t) where T : new()
     {
    
     }
    

    注意CLR目前只支持无参构造方法约束;

  4. 指定参数必须是指定的基类、或者派生自指定的基类;

  5. 指定参数必须是指定的接口,或者实现指定的接口;

  6. 指定T提供的类型参数必须是为U提供的参数,或者是派生自为U提供的参数;

    1
    2
    3
    4
    5
    6
    7
    
    public class Sample<U>
    {
        public void Method1<T>(T t) where T : U
        {
    
        }
    }
    
  7. 在编程过程中应该考虑为泛型参数设定约束,约束使泛型参数成为一个实实在在的“对象”, 让它具有了我们想要的行为和属性,而不仅仅是一个object。

建议35 使用default为泛型类型指定初始值

有些算法,比如泛型集合List中的Find算法, 所查找的对象可能会是值类型,也可能会是引用类型;在这种算法的内部,我们常常会为这些值类型或者引用类型变量指定默认值。于是,问题来了:值类型变量的默认初始值时0值,而引用类型的变量的默认初始值时null值,显然,这会导致下面的编译出错:

1
2
3
4
5
public T Func<T>()
{
    T t = null;
    return t;
}

CS0403 Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using default('T') instead.

无法将 null 转换为类型参数“T”,因为它可能是不可以为 null 的值类型。 请考虑改用 default(‘T’)。

1
2
3
4
5
public T Func<T>()
{
    T t = 0;
    return t;
}

CS0029 cannot implicitly convert type 'int' to 'T', 无法将类型’int’隐式转换为’T‘;

因此,上述的代码可以改为:

1
2
3
4
5
public T Func<T>()
{
    T t = default(T);
    return t;
}

这样,如果它在运行时碰到T是一个整型,那么运行时为其赋值为0;如果T在运行时是一个Person这样的引用类型,则会为其赋null值;

下面展示一段Find的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[__DynamicallyInvokable]
public T Find(Predicate<T> match)
{
    if (match == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    
    for (int i = 0; i < this; i++) {
        if (match(this._items[i])) {
            return this._items[i];
        }
    }
    return default(T);
}

建议36 使用FCL中的委托声明

FCL中存在3类这样的委托声明,它们分别是Action,Func,Predicate。尤其是在它们的泛型版本出来以后,已经能够满足我们在实际编码过程中的大部分编码的需求;

  • Action表示接受0个或多个输入参数,执行一段代码,没有任何的返回值;
  • Fun表示接受0个或多个输入参数,执行一段代码,带有返回值;
  • Predicate表示定义一组条件并判定参数是否符合条件;

Action的重载版本有17个,最多参数的重载有16个参数;

Fun的重载版本有17个,最多参数的重载有16个参数;

Note:很少方法的参数能够超过16个,如果真有这样的参数,首先要考虑自己的函数设计是不是有问题;另外,也可以采用params关键字来减少参数的声明。如:

static void SomeMethod(params int[] i)

表示该方法接受0个或多个整型参数;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

namespace Advice36
{
    internal class Program
    {
        delegate int AddHandler(int i, int j);
        delegate void PrintHandler(string msg);

        static void Main(string[] args)
        {
            AddHandler add = Add;
            PrintHandler print = Print;

            print(add(1, 2).ToString());
        }

        static int Add(int i, int j)
        {
            return i + j;
        }

        static void Print(string msg)
        {
            Console.WriteLine(msg);
        }
    }
}

委托声明AddHandlerPrintHandler完全可以被Func与Action取代。

1
2
3
4
5
Func<int, int, int> add = Add;

Action<string> print = Print;

print(add(1, 2).ToString());

我们应该习惯在代码中使用这类委托来代替自己的委托声明;

除了Action, Func, 和Predicate外, FCL中还有用于表示特殊含义的委托声明;像下面的几种:

用于表示注册事件方法的委托声明:

1
2
public delegate void EventHandler(object sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

表示线程方法的委托声明:

1
2
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);

表示异步回调的委托声明:

1
public delegate void AsyncCallback(IAsyncResult ar);

在FCL中每一类委托声明都代表一类特殊的用途,虽然可以使用自己的委托声明来代替,但是这样做没必要,而且会让代码失去简洁性和标准性;在我们实现自己的委托声明前,应该首先查阅官方文档后确认有必要才这样做才比较好;

建议37 使用lambda表达式代替方法和匿名方法

在上一条建议中, 我们使用了Func和Action委托, 实现了加法和加法结果的打印;

实际上要完成相同相同的功能,还有很多种编码的方法。我们先来看一种中规中矩的,也是最繁琐的写法;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

namespace Advice37
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Func<int, int, int> add = new Func<int, int, int>(Add);
            Action<string> print = new Action<string>(Print);

            print(add(1, 2).ToString());
        }

        static int Add(int i, int j)
        {
            return i + j;
        }

        static void Print(string msg)
        {
            Console.WriteLine(msg);
        }
    }
}

Note: 上面的语法虽然繁琐,但是我们可以从中加深对委托的理解;委托也是一种数据类型,跟任何FCL中的引用类型没有差别;

也可以使用匿名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

namespace Advice37
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Func<int, int, int> add = new Func<int, int, int>(delegate (int i, int j)
            {
                return i + j;
            });

            Action<string> print = new Action<string>(delegate (string msg)
            {
                Console.WriteLine(msg);
            });

            print(add(1, 2).ToString());
        }
    }
}

使用匿名方法后,我们就不需要再在Main外额外声明两个方法了,可以直接在Main这个工作方法完成所有代码的编写,而不会影响代码的清晰性,实际上所有代码不超过3行的方法(条件是它不会被重用),我们都建议采用这种方式来编写;

上面的改进版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Func<int,int,int> add = delegate(int i, int j)
{
    return i + j;
};

Action<string> print = delegate(string msg)
{
    Console.WriteLine(msg);
};

print(add(1, 2).ToString());

最终版本还是演化为使用lambda表达式的方式:

1
2
3
4
5
Func<int, int, int> add = (i, j) => i + j;

Action<string> print = msg => Console.WriteLine(msg);

print(add(1, 2).ToString());

Lambda表达式操作符”=>“的左侧是方法的参数, 右侧是方法体,其本质还是匿名方法。实际上,经过编译后的Lambda表达式就是一个匿名方法。(这点也可以用过反编译来验证)。我们应当在实际编码中熟练的运用lambda, 避免出现繁琐而不美观的代码;

建议38 小心闭包中的陷阱

观察一下下面的代码, 试着设想一下输出的代码时什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;

namespace Advice38
{
    internal class Program
    {
        static void Main(string[] args)
        {
            List<Action> lists = new List<Action>();
            for (int i = 0; i < 5; i++)
            {
                Action t = () =>
                {
                    Console.WriteLine(i.ToString());
                };
                lists.Add(t);
            }

            foreach (var t in lists)
            {
                t();
            }
        }
    }
}

我们这段代码的预期是,让匿名方法(这里表现为lambda表达式)接受参数i, 并输出:

0 1 2 3 4

但是实际上的输出是: 5 5 5 5 5

这段代码并不像看起来的那么简单,要完全理解运行时候的代码是如何运行的,首先我们得理解C#编译器为我们做了些什么?

反编译查看IL代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       133 (0x85)
  .maxstack  3
  .locals init (class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action> V_0,
           class Advice38.Program/'<>c__DisplayClass0_0' V_1,
           class [mscorlib]System.Action V_2,
           int32 V_3,
           bool V_4,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action> V_5,
           class [mscorlib]System.Action V_6)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  newobj     instance void Advice38.Program/'<>c__DisplayClass0_0'::.ctor()
  IL_000c:  stloc.1
  IL_000d:  ldloc.1
  IL_000e:  ldc.i4.0
  IL_000f:  stfld      int32 Advice38.Program/'<>c__DisplayClass0_0'::i
  IL_0014:  br.s       IL_003d
  IL_0016:  nop
  IL_0017:  ldloc.1
  IL_0018:  ldftn      instance void Advice38.Program/'<>c__DisplayClass0_0'::'<Main>b__0'()
  IL_001e:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_0023:  stloc.2
  IL_0024:  ldloc.0
  IL_0025:  ldloc.2
  IL_0026:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::Add(!0)
  IL_002b:  nop
  IL_002c:  nop
  IL_002d:  ldloc.1
  IL_002e:  ldfld      int32 Advice38.Program/'<>c__DisplayClass0_0'::i
  IL_0033:  stloc.3
  IL_0034:  ldloc.1
  IL_0035:  ldloc.3
  IL_0036:  ldc.i4.1
  IL_0037:  add
  IL_0038:  stfld      int32 Advice38.Program/'<>c__DisplayClass0_0'::i
  IL_003d:  ldloc.1
  IL_003e:  ldfld      int32 Advice38.Program/'<>c__DisplayClass0_0'::i
  IL_0043:  ldc.i4.5
  IL_0044:  clt
  IL_0046:  stloc.s    V_4
  IL_0048:  ldloc.s    V_4
  IL_004a:  brtrue.s   IL_0016
  IL_004c:  nop
  IL_004d:  ldloc.0
  IL_004e:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::GetEnumerator()
  IL_0053:  stloc.s    V_5
  .try
  {
    IL_0055:  br.s       IL_006a
    IL_0057:  ldloca.s   V_5
    IL_0059:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>::get_Current()
    IL_005e:  stloc.s    V_6
    IL_0060:  nop
    IL_0061:  ldloc.s    V_6
    IL_0063:  callvirt   instance void [mscorlib]System.Action::Invoke()
    IL_0068:  nop
    IL_0069:  nop
    IL_006a:  ldloca.s   V_5
    IL_006c:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>::MoveNext()
    IL_0071:  brtrue.s   IL_0057
    IL_0073:  leave.s    IL_0084
  }  // end .try
  finally
  {
    IL_0075:  ldloca.s   V_5
    IL_0077:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>
    IL_007d:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0082:  nop
    IL_0083:  endfinally
  }  // end handler
  IL_0084:  ret
} // end of method Program::Main

IL_0007行: newobj instance void Advice38.Program/'<>c__DisplayClass0_0'::.ctor(), 为我们创建了 一个类<>c__DisplayClass0_0, 并且在循环内部每次会为这个类的一个实例变量 i 赋值。

这个类的IL代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass0_0'
       extends [mscorlib]System.Object
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
   
  .method assembly hidebysig instance void 
        '<Main>b__0'() cil managed
{
  // 代码大小       19 (0x13)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldflda     int32 Advice38.Program/'<>c__DisplayClass0_0'::i
  IL_0007:  call       instance string [mscorlib]System.Int32::ToString()
  IL_000c:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0011:  nop
  IL_0012:  ret
} // end of method '<>c__DisplayClass0_0'::'<Main>b__0'
    
    
  .field public int32 i
} // end of class '<>c__DisplayClass0_0'

//ctor
.method public hidebysig specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // 代码大小       8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  ret
} // end of method '<>c__DisplayClass0_0'::.ctor

经过分析,会发现前面的这段代码实际和下面这段代码是一致的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void Main(string[] args)
{
    List<Action> lists = new List<Action>();
    TempClass tempClass = new TempClass();
    for (tempClass.i = 0; tempClass.i < 5; tempClass.i++)
    {
        Action t = tempClass.TempFuc;
        lists.Add(t);
    }
    foreach (Action t in lists)
    {
    	t();
    }
}

class TempClass
{
    public int i;
    public void TempFuc()
    {
    	Console.WriteLine(i.ToString());
    }
}

这段代码演示的就是闭包对象。所谓闭包对象,指的是上面这种情形中的TempClass对象(在第一段代码中,就是编译器为我们生成的<>c__DisplayClass0对象)。如果匿名方法(lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到闭包对象中,即将for循环中的变量 i 修改成了引用闭包对象的公共变量 i 。这样,即使代码执行离开了原局部变量 i 的作用域(如for循环),包含该闭包对象的作用域还存在。理解了这一点,就理解了代码的输出了。

要实现本建议开始时所预期的输出,可以将闭包对象的产生放在for循环内部:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 static void Main(string[] args)
 {
     List<Action> lists = new List<Action>();
     for (int i = 0; i < 5; i++)
     {
         int temp = i;
         Action t = () =>
         {
         	Console.WriteLine(temp.ToString());
         };
         lists.Add(t);
         }
         foreach (Action t in lists)
         {
         	t();
         }
 }

也可以这样改写代码, 和上面的一致:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{
    List<Action> lists = new List<Action>();
    for (int i = 0; i < 5; i++)
    {
        TempClass tempClass = new TempClass();
        tempClass.i = i;
        Action t = tempClass.TempFuc;
        lists.Add(t);
    }
    foreach (Action t in lists)
    {
    	t();
    }
}

class TempClass
{
    public int i;
    public void TempFuc()
    {
    	Console.WriteLine(i.ToString());
    }
}

建议39 了解委托的实质

理解C#中的委托需要把握那两个要点:

  1. 委托是方法指针;
  2. 委托是一个类,当对其进行实例化的时候,要将引用方法作为它的构造方法的参数。

设想这样一个场景:在点对点文件传输过程当中,我们要设计一个文件传输类,该传输类起码要满足下面几项功能:

  • 可以传输文件;
  • 可以按照按照百分制通知传输的进度;
  • 传输类能同时被控制台应用程序和WinForm应用程序使用;

由于要让通知本身能够被控制台程序和WinForm程序使用,因此设计这个文件传输类在进行进度通知时,就不能显式调用;

1
2
3
4
5
Console.WriteLine("当前进度:"+fileProgress);

// or

this.progressText.Text = "当前进度:" + fileProgress;

理想情况下,在需要通知的地方,全部将其置换成一个方法的指针,由调用者来决定该方法完成什么样的功能。这个方法指针在C#中就是委托。可以像下面那样声明委托:

public delegate void FileUploadedHandler(int progress);

这个文件传输类可以写成这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class FileUploader
{
    public delegate void FileUploadedHandler(int progress);
    public FileUploadedHandler FileUploaded;

    public void Upload()
    {
        int fileProgress = 100;
        while (fileProgress > 0)
        {
            //传输代码,省略
            fileProgress--;
            if (FileUploaded != null)
            {
                FileUploaded(fileProgress);
            }
        }
    }
}

调用者在调用这个文件传输类的时候,应该同时为FileUploaded赋值,赋值过程中也就是将自身所具有的和委托声明相同的声明方法赋值给FileUploaded。这样,类型FileUploader在执行到下面的代码时,就是执行调用者自身的方法:

FileUploaded(fileProgress);

理解了”委托是方法指针“这一点后,再去理解委托实际上是一个类;

实际上我们应该把public delegate void FileUploadedHandler(int progress); 理解成如下的一个类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;

namespace Advice39
{
    class FileUploadedHandler : MulticastDelegate
    {
        public FileUploadedHandler(object @object, IntPtr intPtr)
        {

        }

        public virtual IAsyncResult BeginInvoke(int progress, AsyncCallback callback, object @object)
        {

        }

        public virtual void EndInvoke(IAsyncResult result)
        {

        }

        public virtual void Invoke(int progress)
        {

        }
    }
}

委托是一个类, 这个工作由编译器为我们完成, 使用ILDasm.exe查看, 我们可以看到IL代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.class auto ansi sealed nested public FileUploadedHandler
    extends [mscorlib]System.MulticastDelegate
{
    .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
    {
    }

    .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(int32 progress, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
    {
    }

    .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
    {
    }

    .method public hidebysig newslot virtual instance void Invoke(int32 progress) runtime managed
    {
    }
}

调用委托方法, FileUploaded(fileProgress);

实际上是调用: FileUploaded.Invoke(fileProgress);

可以进一步的查看Upload方法的IL代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.method public hidebysig instance void Upload() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 fileProgress,
        [1] bool CS$4$0000)
    L_0000: nop 
    L_0001: ldc.i4.s 100
    L_0003: stloc.0 
    L_0004: br.s L_0028
    L_0006: nop 
    L_0007: ldloc.0 
    L_0008: ldc.i4.1 
    L_0009: sub 
    L_000a: stloc.0 
    L_000b: ldarg.0 
    L_000c: ldfld class MyTest.FileUploader/FileUploadedHandler MyTest.FileUploader::FileUploaded
    L_0011: ldnull 
    L_0012: ceq 
    L_0014: stloc.1 
    L_0015: ldloc.1 
    L_0016: brtrue.s L_0027
    L_0018: nop 
    L_0019: ldarg.0 
    L_001a: ldfld class MyTest.FileUploader/FileUploadedHandler MyTest.FileUploader::FileUploaded
    L_001f: ldloc.0 
    L_0020: callvirt instance void MyTest.FileUploader/FileUploadedHandler::Invoke(int32)
    L_0025: nop 
    L_0026: nop 
    L_0027: nop 
    L_0028: ldloc.0 
    L_0029: ldc.i4.0 
    L_002a: cgt 
    L_002c: stloc.1 
    L_002d: ldloc.1 
    L_002e: brtrue.s L_0006
    L_0030: ret 
}

可以看到L_0020处调用Invoke方法。一句话:委托是一种数据类型,它用来传递方法。

建议40 使用event关键字对委托施加保护

上一个建议中实现了一个文件传输类:FileUploader

如果像这样调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void Main(string[] args)
{
    FileUploader f1=new FileUploader();
    f1.FileUploaded = Progress;
    f1.FileUploaded = ProgressAnother;
    f1.Upload();

    Console.Read();
}

static void Progress(int progress)
{
	Console.WriteLine(progress);
}

static void ProgressAnother(int progress)
{
	Console.WriteLine("另一个方法:{0}", progress);
}

以上调用者代码本身是和FileUploader类一起的,这起码存在两个问题:

  • 如果在Main中另起一个工作线程,该工作线程则可以将FileProgress委托链置空;

f1.FileUploaded = null

  • 可以在外部调用FileUploaded, 例如:

f1.FileUploaded(10) ;

这应该是不被允许的,**因为什么时候通知调用者,应该是FileUploader类自己的职责,而不是调用者本身来决定的。**event关键字正是在这种情况下被提出来的,它为委托加了保护。

public FileUploadedHandler FileUploaded; 改为 public event FileUploadedHandler FileUploaded;

这样,上面提到的几种情况就会被阻止:

1
2
3
f1.FileUploaded = null;
f1.FileUploaded = Progress;
f1.FileUploaded = ProgressAnother;

以上代码通过不了编译,事件”MyTest.FileUploader.FileUploaded“只能出现在 += 或 -=的左边;

(从类型“MyTest.FileUploader”中使用时除外)

建议41 实现标准的事件模型

上一个建议中,实现了一个带事件通知的文件传输类FileUploader虽然已经满足需求,但却不符合C#的编码规范,查看一下EventHandler的原型声明:

public delegate void EventHandler(object sender, EventArgs e);

我们应该去了解Microsoft为事件模型设定的几个规范:

  • 委托类型的名称以EventHandler结束;
  • 委托原型返回值为void;
  • 委托原型具有两个参数: sender表示事件发送者, e表示事件参数

为了将FileUploader的修改得符合C#的编码规范,首先提供一个FileUploadedEventArgs类来保存进度信息:

1
2
3
4
class FileUploadedEventArgs : EventArgs
{
    public int FileProgress{ get; set;}
}

而对FileUploader类型也做出相应修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class FileUploader
{
    public event EventHandler<FileUploadedEventArgs> FileUploaded;

    public void Upload()
    {
        FileUploadedEventArgs e = new FileUploadedEventArgs() { FileProgress = 100 };
        while (e.FileProgress > 0)
        {
            //传输代码,省略
            e.FileProgress--;
            if (FileUploaded != null)
            {
            	FileUploaded(this, e);
            }
        }
    }
}

最终,调用代码看起来像下面这个样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void Main(string[] args)
{
    FileUploader f1=new FileUploader();
    f1.FileUploaded += f1_FileUploaded;
    f1.Upload();
}

static void f1_FileUploaded(object sender, FileUploadedEventArgs e)
{
    Console.WriteLine(e.FileProgress);
}

建议42 使用泛型参数兼容泛型接口的不可变性

让返回值类型返回比声明的类型派生程度更大的类型, 就是”协变“;协变不是一种新出现的计数,我们在以往编程中会不自觉的使用协变,以下的代码就是不自觉应用协变的例子:

1
2
3
4
5
public Employee GetAEmployee(string name)
{
    Console.WriteLine("我是雇员:"+name);
    return new Programmer() { Name = name };//Programmer是Employee的子类
}

Programmer是Employee的子类,所以Programmer对象也是Employee对象。方法GetAEmployee返回一个Programmer的对象,也就是相当于返回一个Employee对象。

由于协变是一种如此自然的应用,我们很可能写出如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Program
{
    static void Main(string[] args)
    {
        ISalary<Programmer> s = new BaseSalaryCounter<Programmer>();
        PrintSalary(s);
    }

	static void PrintSalary(ISalary<Employee> s)        
	{            
		s.Pay();        
	}
}

interface ISalary<T>
{
	void Pay();
}

class BaseSalaryCounter<T> : ISalary<T>
{
    public void Pay()
    {
    	Console.WriteLine("Pay base salary");
    }
}

class Employee
{
	public string Name { get; set; }
}

class Programmer : Employee
{
}

class Manager : Employee
{
}

PrintSalary这个方法中,方法接收的类型是ISalary<Employee>。于是,我们想当然的认为ISalary<Programmer>必然也可以被PrintSalary方法接收的。事实却不然,代码会编译失败:

无法从“MyTest.ISalary<MyTest.Programmer>”转换为“MyTest.ISalary<MyTest.Employee>

编译器对于接口和委托类型参数的检查是非常严格的,除非用关键字out特别声明,不然这段代码只会编译失败。要让PrintSalary完成需求,我们可以使用泛型类型参数:

修改为:

1
2
3
4
static void PrintSalary<T>(ISalary<T> s)
{
    s.Pay();
}

Note:建议开头指出“协变”是针对返回值而言的,但是所举的这个例子并没有体现“返回值”这个概念。实际上,只要泛型类型参数在一个接口声明中不被用来作为方法的输入参数,我们就姑且把它看成是“返回值”类型的。所以,本建议中这种模式是满足“协变”定义的。但是,只要将T作为输入参数,就不满足“协变”定义了。

建议43 让接口中的泛型参数支持协变

除了上述建议中提到的使用泛型参数兼容接口不可变性外,还有一种方法是为接口中的泛型声明out关键字来支持协变, 如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface ISalary<out T>  // 使用out关键字
{
    void Pay();
}

static void Main(string[] args)
{
    ISalary<Programmer> s = new BaseSalaryCounter<Programmer>();
    ISalary<Manager> t = new BaseSalaryCounter<Manager>();
    PrintSalary(s);
    PrintSalary(t);
    Console.ReadLine();
}

static void PrintSalary(ISalary<Employee> s)
{
    s.Pay();
}

​ out关键字是FCL4.0中新增的功能,它可以在泛型接口和委托中使用,用来让类型参数支持协变性。通过协变,可以使用比声明的参数派生类型更大的参数。

​ 以上这段代码在FCL4.0中以前的版本是不能编译通过的,因为IEnumerable这个接口在FCL中没有被声明为IEnumerable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static void Main(string[] args)
{
    IList<Programmer> programmers = new List<Programmer>();
    IList<Manager> managers = new List<Manager>();
    PrintPersonName(programmers);
    PrintPersonName(managers);
}

static void PrintPersonName(IEnumerable<Employee> persons)
{
    foreach (var person in persons)
    {
        Console.WriteLine(person.Name);
    }
}

FCL4.0对多个接口进行了修改以支持协变,如IEnumerable<out T>, IEnumerator<out T>, IQueryable<out T>等, 由于IEnumerable现在支持协变,因此第一段代码在FCL4.0能运行得很好;

​ 在我们自己得代码,如果要编写泛型接口,除非确定该接口中得泛型参数不涉及变体,否则都建议加上out关键字。协变增大了接口得可使用范围,而且几乎不会带来什么Side impact;

建议44 理解委托中的协变

委托中的泛型变量是天然”部分“支持协变的,为什么说是“部分支持”呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

namespace Advice44
{
    class Program
    {
        public delegate T GetEmployeeHandler<T>(string name);

        static void Main(string[] args)
        {
            GetEmployeeHandler<Employee> getAEmployee = GetAManager;
            Employee e = getAEmployee("bomin");
            Console.ReadLine();
        }

        static Manager GetAManager(string name)
        {
            Console.WriteLine("我是经理" + name);
            return new Manager() { Name = name };
        }

        static Employee GetAEmployee(string name)
        {
            Console.WriteLine("我是雇员: " + name);
            return new Employee() { Name = name };
        }

    }

    class Employee
    {
        public string Name { get; set; }
    }

    class Manager : Employee
    {
    }
}

上面的GetAManager返回的是一个Manager,但是在使用中,其实是将其赋值给一个泛型参数为Employee的委托变量。因为存在下面一种情况,所以编译不过:

1
2
GetEmployeeHanlder<Manager> getAManager = GetAManager;
GetEmployeeHanlder<Employee> getAEmployee = getAManager;

要让上面的代码编译通过, 同样需要为委托中的泛型参数指定out关键字:

public delegate T GetEmployeeHandler<T>(string name);

除非考虑到该委托声明肯定不会用于可变性,否则,为委托中的泛型参数指定out关键字将会拓展该委托的应用,建议在实际编码过程中永远这样使用。实际上,FCL4.0中的一些委托声明已经用out关键字来让委托支持协变了,像我们常常会使用到的:

1
2
3
public delegate TResult Func<out TResult>();

public delegate TOutput Converter<in TInput, out TOutput>(TInput input);

建议45 为泛型类型参数指定逆变

逆变是指方法的参数可以是委托或泛型接口的参数类型的基类FCL4.0支持逆变的常用委托有:

1
2
3
Func<int T,out TResult>

Predicate<in T>

常用泛型接口有:

IComparer<in T>

以下示例演示了泛型类型参数执行逆变所带来的好处:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Program
{
    static void Main()
    {
        Programmer p = new Programmer { Name = "Mike" };
        Manager m = new Manager { Name = "Steve" };
        Test(p, m);
    }

    static void Test<T>(IMyComparable<T> t1, T t2)
    {
        // 省略
    }
}

public interface IMyComparable<in T>
{
    int Compare(T other);
}

public class Employee : IMyComparable<Employee>
{
    public string Name { get; set; }
    public int Compare(Employee other)
    {
        return Name.CompareTo(other.Name);
    }
}

public class Programmer : Employee, IMyComparable<Programmer>
{
    public int Compare(Programmer other)
    {
        return Name.CompareTo(other.Name);
    }
}

public class Manager : Employee
{
}

上面的例子中,如果不为接口IMyComparable的泛型参数T指定in关键字,将会导致Test(p,m)编译错误。由于引入了接口的逆变性,这让方法Test支持了更多的场景。在FCL4.0之后的版本的实际编码中应该始终注意这一点。