这篇文章我们来用一下泛型委托,委托的泛型如果我们用最原始的角度去看,那就是在委托声明后加上一个<T>。
但是,关于委托的内容还不止这些,首先先看看C#给开发者自带的两种泛型委托:
Action委托:
Action委托主要用于无参或者无返回值的委托的委托,那么使用起来非常简单:
若有参数,则表示起来就是:
class Program
{
static void Main(string[] args)
{
Calculate cal = new Calculate();
Action<int, int> action = new Action<int, int>(cal.Show);
action.Invoke(1, 2);
}
}
public class Calculate
{
public void Show(int a, int b)
{
Console.WriteLine((a + b).ToString());
}
}
输出的结果是:
注意,Action委托里面的方法都是无返回值的。
Func委托
与Action委托不同的是,Func委托的最后一个泛型参数总是所要包装的函数的返回值。前面的为参数列表的类型。
我们把上个例子改成我们需要的样子:
class Program
{
static void Main(string[] args)
{
Calculate cal = new Calculate();
Func<int, int,string> func = new Func<int, int,string>(cal.Show);
Console.WriteLine(func.Invoke(1, 2));
}
}
public class Calculate
{
public string Show(int a, int b)
{
return (a + b).ToString();
}
}
如图,声明了三个int,形参里只有两个int,最后是string,也就是Show函数的返回值。 输出结果:
自定义泛型委托
我们如果要实现类似于上文的Func和Action委托的形式,也是很简单的,我们只需要自己定义委托的格式。
public delegate TResult FanHandler<in T1, in T2, out TResult>(T1 a, T2 b);
class Program
{
static void Main(string[] args)
{
Calculate cal = new Calculate();
FanHandler<int, int, string> fan = new FanHandler<int, int, string>(cal.Show);
Console.WriteLine(fan.Invoke(1, 2));
}
}
public class Calculate
{
public string Show(int a, int b)
{
return (a + b).ToString();
}
}
和func委托里面定义是一样一样的。
参数的可变性
在代码中,对于一个方法,有通过参数列表输入方法体的参数,也存在通过返回值输出的参数。为了使代码的输入输出更加灵活,例如可以对一个参数输入其父类的对象(逆变),对返回值输出其子类的对象(协变),这两个在C#中统称为可变性。这个概念看似比较呕哑嘲哳,但是在我们的代码的一些规范里已经有体现了。
在上面的官方定义的泛型委托中,我们发现这些泛型委托定义的时候定义的时候,前面都会有in和out两个参数,这两个就是可变性在泛型中的体现。
- in:逆变量。指方法参数的兼容性。作为参数时,它可以从一个类转为声明的该类的派生类。
- out:协变量。指方法返回类型的兼容性作为返回值时,它可以从一个类转为声明该类的基类。
- 二者不能逆转
我们不难看到,若需要包装有返回值的方法,我们的委托是要有返回值的类型的,但是,如果说返回值是个泛型,那么我们定义的时候就需要多定义一个泛型出来表示返回值的泛型,并加上前缀关键字:out。若只是参数的泛型,则使用前缀关键字:in。如果我们不需要这些参数与返回值的区分,那么直接使用不加前缀的泛型就好了。
由于泛型可变性标识符存在,我们代码中使用泛型时的灵活性大大提高了,它可以使代码中的泛型能传入的类型更加广泛而且可控,我们使用另一个官方的泛型委托Converter来实现泛型可变性,在Converter中,两个参数一个是逆变,第二个是协变:
class Program
{
static void Main()
{
Converter<object, string> converter = x => x.ToString();
Converter<TestClass1, string> con1 = converter;
Converter<object, object> con2 = converter;
Converter<TestClass1, object> con3 = converter;
}
}
class TestClass
{ }
由于TestClass的父类就是object,所以可以将逆变量类型object类型改为子类的TestClass,将协变量类型string改为其父类object。
可变性一般存在于委托、泛型委托、泛型接口中,但只有泛型中的可变性是有关键字标识的。
委托中的可变性
逆变:逆变在委托中的体现在事件处理器的参数列表中,在事件参数EventArgs中,如果自定义事件参数,自定义事件参数类型必须继承自官方事件参数类型EventArgs,这里即使用了委托的逆变性,通过增加父类,让其他格式一致的事件接收器也能订阅这个事件。我们下文中的例子就体现了委托参数的逆变性:TestEventHandler的事件参数为EventArgs也能被事件参数为MyEventArgs的FunctionTest所订阅。
public delegate void TestEventHandler(object sender, MyEventArgs e);
class Program
{
static void Main()
{
EventClass Myevent = new EventClass();
Myevent.handlerEvent += FunctionTest;
Myevent.OnHandler();
}
static void FunctionTest(object sender,EventArgs e)
{
Console.WriteLine("逆变性调用了");
}
}
public class MyEventArgs:EventArgs
{ }
public class EventClass
{
public void OnHandler()
{
MyEventArgs myArgs = new MyEventArgs();
handler.Invoke(this,myArgs);
}
private TestEventHandler handler;
public event TestEventHandler handlerEvent
{
add
{
handler += value;
}
remove
{
handler -= value;
}
}
}
输出为:
协变:委托中协变的例子不多,但是很多地方仍然可以使用到委托的协变,我们写一个关于Stream的例子,这样的例子在之后序列化那一章将会具体介绍:
public delegate Stream StreamDelegate();
class Program
{
static void Main()
{
StreamFactory getStream = new StreamFactory();
StreamDelegate streamDelegate = getStream.GetStream;
Stream stream = streamDelegate.Invoke();
Console.WriteLine(stream.GetType());
}
}
class StreamFactory
{
public MemoryStream GetStream()
{
return new MemoryStream();
}
}
在这个地方,委托的返回值是Stream这个大类,但所订阅的事件的返回值却可以是Stream的子类MemoryStream。输出:
接口的可变性
接口的可变性存在于各种接口的声明里,例如比较熟知的枚举接口IComparer<T>(逆变量)和IEnumerable<T>(协变量)中,我们写一个关于迭代器的协变的例子,使用了:
class Program
{
static void Main()
{
Test test = new Test();
IEnumerable<First> ienumerable = test.ETest();
IEnumerator enumerator = ienumerable.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
class Test
{
public IEnumerable<Second> ETest()
{
yield return new Second();
yield return new Second();
yield return new Second();
}
}
class First
{ }
class Second : First
{ }
如上图,声明泛型类型为First的迭代器块可以返回子类Second的枚举接口。这里就使用了返回值的协变量,输出如下图:
C#中可变性的限制
C#中对可变性的支持主要来自于CLR,但其他的限制也不少
1.只有委托和接口的参数存在可变性,对于类或方法的泛型来说是没有可变性这个概念的。CLR不允许这样声明。
2.可变性只支持操作引用类型,对于值类型来说没这回事情,我们可以说可变性只支持引用转换。
3.对于赋值参数关键字out(与引用参数关键字ref对应的那个)来说,它没有可变性,不要和协变关键字out弄混了,对于CLR来说,out只是应用了[out]特性的ref参数。
4.可变性必须显式指定。
5.泛型可变性不能用于多播委托。
6.如果泛型存在内嵌,例如泛型委托的泛型为逆变量,但返回一个协变量的Func委托,如下图这样是不成立的,代码会报错:
public delegate Func<T> getFunc<in T>();
同样的,如果泛型委托的泛型为协变量,但是参数中有逆变量的Action委托,也会报同样的错误,这是泛型的逆变协变非常“疯狂”的用法,我们可以说,内嵌的逆变量反转了它的可变性,但是协变量不变。这句话摘自《C# in Depth》但是我自己看到的却是二者的可变性都发生了反转,这个概念确实太绕了,一点儿可读性都没有了。我们要在代码中规避这样的写法。
这样是对的:
这样却是错的: