泛型,委托和事件

基于泛型, 我们得以将类型参数化, 以便更大范围地进行代码复用. 同时减少了泛型类及泛型方法中的转型, 确保了类型安全. 委托本身是一种音乐类型, 它保存的也是托管堆中对象的引用, 只不过这个引用比较特殊, 它是对方法的引用. 事件本身也是委托, 它是委托组, 用关键字event来对事件进行区分.

建议32 : 总是优先考虑泛型

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

  • 可重用性
    T理解为一个占位符, C#泛型编译生成的IL代码中, T就是一个占位符的角色.在运行时, 即时编译器JIT会用实际代码中输入的T类型来代替T. MyList<string>MyList<int>是2个完全不同的类型, 但是对本地代码而言只有一个泛型MyList<T>类.

  • 类型安全
    如果针对object编写代码, 则会出现类型安全性的问题,list[0] = 123; list[1] = "123";都能编译通过.

  • 高效
    转型的效率损耗,尤其是值类型,会带来装箱和拆箱的性能损耗.

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

MyList<string>MyList<int>是2个完全不同的类型, 所以不应该将MyList<T>中的静态成员理解成MyList<string>MyList<int>的共有的成员. 它们之间是不共享静态成员的. 但是如果数据类型是一致的(T类型相同),那么还是可以共享静态成员的.

上面举的例子是基于泛型类型的, 但是, 非泛型类型中的泛型方法并不会在运行时的本地代码中生成不同的类型。

static void Main(string[] args)
{
    Console.WriteLine(MyList.Func<int>()); // 0
    Console.WriteLine(MyList.Func<int>()); // 1
    Console.WriteLine(MyList.Func<string>());// 2
}

class MyList
{
    static int count;
    public static int Func<T>()
    {
        return count++;
    }
}

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

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

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

class SalaryComputer
{
   public int Compare<T>(T t1, T t2)
   {
      retrun 0;
   }
}

但是,在加了约束之后,我们会发现参数t1和t2变成了一个有用的对象。由于为其指定了对应的类型,t1 和t2现在就是一个Salary了,在方法的内部,它拥有了属性BaseSalary和Bonus,代码如下所示。

class SalaryComputer
{
   public int Compare<T>(T t1, T t2) where T : Salary
   {
       if (t1.BaseSalary > t2.BaseSalary)
       {
          retrun 1;
       }
   }
}

可以为泛型指定的约束:

  • 指定参数时值类型(Nullable除外)
    • where T : struct
  • 指定参数时引用类型
    • where T : class
    • where T : Salary
    • object不能用来作为约束
  • 指定参数具有无参数的公共构造方法
    • where T : new()
    • CLR目前直只支持无参构造方法约束
  • 指定参数必须是指定的基类, 或派生自指定的基类
  • 指定的参数必须是指定的接口, 或者实现指定的接口
  • 指定T提供的类型参数必须是为U提供的参数, 或者派生自为U提供的参数
    • where T : U
  • 可以对同一个类型的参数应用多个约束, 并且约束自身可以使泛型类型

在编码过程中,应该始终考虑为泛型参数设定约束。正像本建议开始的时候所说,约束使泛型参数成为-一个实实在在的“对象”,让它具有了我们想要的行为和属性,而不仅仅是一个object。

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

在泛型方法中,为T赋值初始值, 因为值类型初始值是0, 引用类型是null,因此要使用default(T)关键字来初始化.

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

  • Action
    • 执行一个操作, 没有返回值
    • 有17个重载
    • Action<T>
  • Func
    • 执行一个操作并返回一个值
    • 有17个重载
    • Func<T, TResult>
  • Predicate
    • 用于定义一组条件并判断参数是否符合条件

除了Action、Func 和Predicate外,FCL中还有用于表示特殊含义的委托声明。如用于表示注册事件方法的委托声明:

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

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

public delegate void Threadstart() ;
public delegate void ParameterizedThreadstart (object obj) ;

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

public delegate void AsyncCallback (IAsyncResult ar);

在FCL中每一类委托声明都代表一类特殊的用途,虽然可以使用自己的委托声明来代替,但是这样做不仅没有必要,而且会让代码失去简洁性和标准性。在我们实现自己的委托声明前,应该首先查看MSDN,确信有必要之后才这样做。

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

// 示例程序
Func<int, int, int> add = Add;
Action<string> 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);
}

实际上要完成相同的功能,还有多种编码方式。先来看一个最中规中矩的,同时也是最烦琐的写法:

Func<int, int, int> add = new Func<int, int, int>(Add);
Action<string> print    = new Action<string>(Print);
print(add(1, 2).ToString());

从以上写法中注意到: Add方法和Print方法实际上都只有一条语句,因此,使用匿名方法也许是一种更好的选择:

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行的方法(条件是它不被重用),我们都建议采用这种方式来编写。上面版本的改进版是:

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表达式:

Func<int, int, int> add = (i, j) =>
{
    return i + j;
};
Action<string> print = (msg) =>
{
    Console.WriteLine(msg);
};
print(add(1, 2).ToString());

Lambda表达式=>左侧是方法的参数,右侧是方法体, 本质是匿名方法.

以下是以List<T>的Find的一个例子:

//第一种写法
//return this.Find(new Predicate<Student>(delegate(Student target)
//{
//    if (target.Name == name)
//    {
//        return true;
//    }
//    else
//    {
//        return false;
//    }
//}));

//第二种写法
//return this.Find(new Predicate<Student>((target) =>
//    {
//        if (target.Name == name)
//        {
//            return true;
//        }
//        else
//        {
//            return false;
//        }
//    }));


//第三种写法
return this.Find( target => target.Name == name );

建议38 : 小心闭包中的陷阱

List<Action> lists = new List<Action>();
for (int i = 0; i < 5; i++)
{
    Action t = () =>
    {
        Console.WriteLine(i.ToString());
    };
    lists.Add(t);
}
foreach (Action t in lists)
{
    t();
}
// 输出
// 5
// 5
// 5
// 5
// 5

观察这段代码的IL代码会发现,编译器默默的为我们创建了一个类, 会和下面这段代码是一致的:

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, 如果匿名方法(Lambda)表达式引用了某个局部变量, 编译器就会自动将该引用提升到该闭包对象中. 即for循环中的变量i修改成了引用闭包的UI性的公共变量i. 这样一来,即使代码执行后离开了原局部变量i的作用域, 包含该闭包对象的作用域也还在.

要实现预期输出01234,可以将闭包对象的产生放在for循环内部:

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();
   }
}

// 和下段代码一致
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 : 了解委托的实质

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

调用委托方法FileUploaded(fileProgress);,其实是调用 FileUploaded.Invoke(fileProgress);

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

防止外部直接调用委托, 因为什么时候通知调用者, 应该是类自己的职责,而不是调用者本身来决定.

// 以下情况在使用了event关键字后编译会出现错误警告
fl.FileUploaded = null;
fl.FileUploaded = Progress;
fl.FileUploaded(10);

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

微软为事件模型设定的几个规范

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

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

让返回值类型返回比声明的类型派生程度更大的类型,就是“协变”

协变不是一种新出现的技术,在以往的编码中,我们已经在不自觉地使用协变。以下的代码就是一个不自觉应用协变的例子.

public Employee GetAEmployee(string name)
{
   // Programmers是Employee的子类, 也就相当于返回了一个Employee对象
   return new Programmer(){ Name = name};
}

只要泛型参数在一个接口声明中不被用来作为方法的输入参数, 我们都可姑且把它看成是”返回值”类型的.

static void Main(string[] args)
{
    ISalary<Programmer> s = new BaseSalaryCounter<Programmer>();
    // 我们可能认为ISalary<Programmer>必然也可以被此方法使用
    // 方法参数如果不是泛型或out标识,就会报错
    PrintSalary(s);
}

// 错误的写法
// 我们可能认为ISalary<Programmer>必然也可以被此方法使用
// 实际是会发生错误的
// static void PrintSalary(ISalary<Employee> s)
// {
//     s.Pay();
// }

// 正确写法
// 使用泛型类型参数
// 用泛型参数兼容泛型接口的不可变性
static void PrintSalary<T>(ISalary<T> s)
{
    s.Pay();
}

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

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

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

除了建议42中提到的使用泛型参数兼容泛型接口的不可变性外, 还有一种办法就是为接口中的泛型声明加上out关键字来支持协变.

static void Main(string[] args)
{
    ISalary<Programmer> s = new BaseSalaryCounter<Programmer>();
    ISalary<Manager> t = new BaseSalaryCounter<Manager>();
    // 因为接口使用out支持协变, 此处就可以使用比ISalary<Employee>参数的派生类型
    PrintSalary(s);
    PrintSalary(t);
}

static void PrintSalary(ISalary<Employee> s)    //用法正确
{
    s.Pay();
}

interface ISalary<out T>    //使用out关键字
{
    void Pay();
}

out可以在泛型接口和委托中使用, 用来让类型参数支持协变性, 通过协变, 可以使用比声明的参数派生类型更大的参数.

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

建议44 : 理解委托中的协变

委托中的泛型变量天然是部分支持协变的。为什么说是“部分支持协变”呢?来看下面的例子:

class Program
{
    public delegate T GetEmployeeHanlder<T>(string name);

    static void Main()
    {
        // 委托定义中的泛型参数是Employee, 实际赋值时返回的是Manager
        GetEmployeeHanlder<Employee> getEmployee = GetManager;
        Employee e = getEmployee("Mike");
    }

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

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

}


class Employee
{
    public string Name { get; set; }
}
class Programmer : Employee
{
}
class Manager : Employee
{
}

委托定义中的泛型参数是Employee, 实际赋值时返回的是Manager. 我们也许会认为委托中的泛型变量不再需要out关键字,这是错误的理解。因为存在下面这样一种情况,所以编译通不过:

GetEmployeeHanlder<Manager> getManager = GetAManager;
GetEmployeeHanlder<Employee> getEmployee = getManager;// 编译通不过
// 需要在委托的泛型参数中添加out关键字, 上句话才能编译通过
public delegate T GetEmployeeHanlder<out T>(string name);

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

public delegate TResult Func<out TResult> ()

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

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

逆变是指方法的参数可以是委托或泛型接口的参数类型的基类。FCL 4.0中支持逆变的常用委托有:

Func<in T, out TResult>

Predicatec<in T>

常用的泛型接口 IComparer<in T>