C# 学习笔记:委托(5):可变性

这篇文章我们来用一下泛型委托,委托的泛型如果我们用最原始的角度去看,那就是在委托声明后加上一个<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》但是我自己看到的却是二者的可变性都发生了反转,这个概念确实太绕了,一点儿可读性都没有了。我们要在代码中规避这样的写法。

这样是对的:

这样却是错的:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值