.net學習筆記之協變和抗變(原創)


對於協變和抗變的這兩個詞的定義,是初次接觸;然而實際應用應該是從用c#語言編寫代碼開始的。

這兩個詞的理解過程非常繞,查看很多資料,再加上敲代碼調試之后才逐漸有點理解它們的含義。

所謂的協變,可以理解成:父類 -> 子類。父類的對象用子類替換,也可以理解成子類當父類用。

所謂的抗變,可以理解成:子類 -> 父類。子類的對象用父類替換,也可以理解成父類當子類用。抗變也常常翻譯為逆變。

在c#的語言中,很多地方的調用已經隱藏了協變和抗變的使用。函數的返回類型默認是抗變的。例如,函數Func的返回類型為string,我們可以將返回的值賦給object對象。

private void button2_Click(object sender, EventArgs e)
{
    //注意這里:Func的返回類型為string, obj的類型為object, string類型繼承自object
    object obj = Func();
}

string Func()
{
    return "這里有協變的應用";
}

函數的參數類型則默認是協變的。例如,函數Act的輸入參數為object類型,實際操作中我們可以將string類型的對象傳給函數。

private void button2_Click(object sender, EventArgs e)
{
    string str = "這是一個string類型的實例, 函數Act的參數為object, 這里有抗變的應用";
    Act(str);
}

void Act(object obj)
{
    return ;
}

再看下面的代碼,兩者一起應用。

private void button2_Click(object sender, EventArgs e)
{
    string str = "這是一個string類型的實例";
    object obj = null;
    obj = Both(str);
}

string Both(object obj)
{
    return "協變和抗變兩者都有";
}

是不是覺得非常熟悉,常常用到?所以本篇開頭我說可能從開始用C#編寫代碼的時候就開始接觸了。

  • 接下來說說我對《c#高級編程》中的泛型接口的協變和抗變的理解。

上面提到了,c#的語法中已經定義了一些協變和抗變的應用。在泛型接口的定義中,如果泛型類型T用out關鍵詞標注,這個泛型接口就是協變的。而且在接口的代碼里面,T只能用作返回類型,不能用作參數類型。

如果泛型類型T用in關鍵詞標注的話,這個接口就是抗變的。在接口的代碼里面,T只能用作函數的參數類型,而不能用作返回類型。

留文備用。如果理解有誤,請在下面勘正。

 

補充一篇轉載:

轉載自:http://www.cnblogs.com/tenghoo/archive/2012/12/04/interface_covariant_contravariant.html
 
1、什么是協變、逆變?

假設:TSub是TParent的子類。
協變:如果一個泛型接口IFoo<T>,IFoo<TSub>可以轉換為IFoo<TParent>的話,我們稱這個過程為協變,IFoo支持對參數T的協變。
逆變:如果一個泛型接口IFoo<T>,IFoo<TParent>可以轉換為IFoo<TSub>的話,我們稱這個過程為逆變,IFoo支持對參數T的逆變。


2、為什么要有協變、逆變?

通常只有具備繼承關系的對象才可以發生隱式類型轉換,如Base b=new sub()。
協變和逆變可以使得更多的類型之間能夠實現隱式類型轉換、類型安全性有了保障。

3、為什么泛型接口要引入協變、逆變?

基於以上原因的同時、許多接口僅僅將類型參數用於參數或返回值。所以支持協變和逆變后泛型的使用上有了更大的靈活性

4、為什么支持協變的參數只能用於方法的返回值?支持逆變的參數只能用於方法參數?


“TParent不能安全轉換成TSub”,是這兩個問題的共同原因。
我們定義一個接口IFoo。
 

    interface IFoo<T>
    {
        void Method1(T param);
        T Method2();
    }

我們看一下協變的過程:IFoo<TSub>轉換成IFoo<TParent>。

Method1:將TSub替換成TParent,Method1顯然存在 TParent到TSub的轉換。

Method2:返回值類型從TSub換成了TParent,是類型安全的。

所以支持協變的參數只能用在方法的返回值中。

再看一下逆變的過程:IFoo<TParent>轉換成IFoo<TSub>。

Method1:將TParent替換成TSub,Method1存在 TSub到TParent的轉換,是類型安全的。

Method2:返回值類型從TParent換成了TSub,是不安全的。

所以支持逆變的參數只能用在方法的參數中。


5、泛型接口支持協變、逆變和不支持協變、逆變的對比?

這其實是對3個問題的補充。

定義一個接口IFoo,既不支持協變,也不支持逆變。

    interface IFoo<T>
    {
        void Method1(T param);
        T Method2();
    }

實現接口IFoo

復制代碼
    public class FooClass<T> : IFoo<T>
    {
        public void Method1(T param)
        {
            Console.WriteLine(default(T));
        }
        public T Method2()
        {
            return default(T);
        }
    }
復制代碼

定義一個接口IBar支持對參數T的協變

    interface IBar<out T>
    {
        T Method();
    }

實現接口IBar

復制代碼
    public class BarClass<T> : IBar<T>
    {
        public T Method()
        {
            return default(T);
        }
    }
復制代碼

 定義一個接口IBaz支持對參數T的逆變

    interface IBaz<in T>
    {
        void Method(T param);
    }

實現接口IBaz

復制代碼
    public class BazClass<T> : IBaz<T>
    {
        public void Method(T param)
        {
            Console.WriteLine(param.ToString());
        }
    }
復制代碼

定義兩個有繼承關系的類型,IParent和SubClass。

復制代碼
    interface IParent
    {
        void DoSomething();
    }
    public class SubClass : IParent
    {
        public void DoSomething()
        {
            Console.WriteLine("SubMethod");
        }
    }
復制代碼

按照協變的邏輯,分別來使用IFoo和IBar。

復制代碼
            //IFoo 不支持對參數T的協變
            IFoo<SubClass> foo_sub = new FooClass<SubClass>();
            IFoo<IParent> foo_parent = foo_sub;//編譯錯誤

            //IBar 支持對參數T的協變
            IBar<SubClass> bar_sub = new BarClass<SubClass>();
            IBar<IParent> bar_parent = bar_sub;
復制代碼

foo_parent = foo_sub 會提示編譯時錯誤“無法將類型“IFoo<SubClass>”隱式轉換為“IFoo<IParent>”。存在一個顯式轉換(是否缺少強制轉換?)”

 

按照逆變的邏輯,分別來使用IFoo和IBaz。
復制代碼
            //IFoo 對參數T逆變不相容
            IFoo<IParent> foo_parent = null;
            IFoo<SubClass> foo_sub = foo_parent;//編譯錯誤

            //IBaz 對參數T逆變相容
            IBaz<IParent> baz_parent = null;
            IBaz<SubClass> baz_sub = baz_parent;
復制代碼

 foo_sub = foo_parent 會提示編譯時錯誤“無法將類型“IFoo<IParent>”隱式轉換為“IFoo<ISub>”。存在一個顯式轉換(是否缺少強制轉換?)”

 

6、.NET4.0對IEnumerable接口的修改?

2.0中的定義:

    public interface IEnumerable<T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }

4.0中的定義:

    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }

可以看到4.0中增加了對協變的支持。

可以在兩個版本試下, 下面的語句在2.0下會報錯。

    List<SubClass> subarr = new List<SubClass>();
    IEnumerable<IParent> parentarr = subarr;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM