Chapter 3 - Item 22 : Support Generic Covariance and Contravariance

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

C# 4.0 以後,泛型有三種修飾型態,分別為:

1. Invariance(不變數):型別不可被替換。
2. Contravariance(反變數):子類別可代表父類別。
3. Covariance(共變數):父類別可代表子類別。

首先來看一個陣列(covariance)的例子。

程式碼:

abstract public class CelestialBody : IComparable<CelestialBody>
{
    public double Mass { get; set; }

    public string Name { get; set; }

    public int CompareTo( CelestialBody other )
    {
        // elided.
    }
}

public class Asteroid : CelestialBody
{
    public static void unsafeVariantArray( CelestialBody[ ] baseItems )
    {
        baseItems[ 0 ] = new Asteroid( ) { Name = "Hygiea", Mass = 8.85e19 };
    }
}

public class Moon : CelestialBody
{
}

public class Planet : CelestialBody
{
}

Client

// Initialize Planet array that replace for CelestialBody.
CelestialBody[ ] array = new Planet[ 5 ]; 

// Throw System.ArrayTypeMismatchException.
Asteroid.unsafeVariantArray( array2 );

// initialize Asterioid array that replace for CelestialBody.
CelestialBody[ ] array2 = new Asterioid[ 5 ];

// Throw System.ArrayTypeMismatchException.
spaceJunk[ 0 ] = new Planet( );

由於共變數的特性,在執行階段會擲出例外(System.ArrayTypeMismatchException)。

為了在設計類別時能夠更明確的指定輸入、輸出泛型類別;C# 4.0 導入了 in, out 泛型修飾字。

1. Invariance 限制了型別需明確相符。

在先前陣列的例子中,由於無法限制初始化型別;造成擲出例外的情況。為解決這個問題,可使用不變數的設計方式。
    
程式碼:

public class Asteroid : CelestialBody
{
    public static void invariantGeneric( IList<CelestialBody> baseItems )
    {
        baseItems.Add( new Asteroid( )
        { Name = "Hygiea", Mass = 8.85e19 } );
    }
}

Client

// Compile Error.
IList<CelestialBody> list = new List<Planet>( );

// Compile OK.
IList<CelestialBody> list2 = new List<CelestialBody>( );

IList<Planet> list3 = new List<Planet>( );

// Compile Error
Asteroid.invariantGeneric( list3 );

2. Covariance 用在輸出型別(out)。
    
程式碼:

public class Planet : CelestialBody
{
    public static void covariantGeneric( IEnumerable<CelestialBody> baseItems )
    {
        foreach ( var thing in baseItems )
        {
            Debug.WriteLine( $"{thing.Name} has mass of {thing.Mass} Kg." );
        }
    }
}

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

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    new T Current { get; }

    // MoveNext( ), Reset( ) inherited from IEnumerator.
}

Client   

// Complile OK.
IEnumerable<CelestialBody> enums = new List<Planet>( );

Planet.covariantGeneric(enums );
Note:在列舉器的 foreach loop 中,無法增減集合中的元素(readonly);因此用子類別(Planet)代替父類別(CelestialBody)是可行的。

3. Contravariance 用在輸入型別(in)。

public interface IComparable<in T>
{
    int CompareTo( T other );
}

當 CelestialBody 透過比較 Mass 實作了 IComparable<T>,同時也意味著其子類別(Planet, Moon, Asteroid)也可以使用相同的比較方式。也就是父類別可以代替子類別的意思。

結論:
1. 避免使用陣列儲存有繼承關係的類別物件。

2. 透過 in, out 修飾字修飾泛型,可更明確的表達設計原意。