做法 22
支援泛型的共變數與反變數
1. 這個 out 也是 共變數?
public static bool TryParse (string? s, IFormatProvider? provider, out int result);
方法參數中的 out 關鍵字: 在 TryParse 方法中的 out int result 是一個輸出參數。這個 out 關鍵字的作用是:
- 表示這個參數是一個輸出參數。
- 方法必須在返回前為這個參數賦值。
- 呼叫方法時,不需要初始化這個參數,但必須在呼叫後使用它。
在泛型接口或委託定義中使用的 out 關鍵字表示協變。例如:
interface IEnumerable<out T> { ... }
這裡的 out 表示:
- T 只能用作輸出(返回值),不能用作輸入(參數)。
- 允許將更具體(派生)類型的集合賦值給更一般(基類)類型的集合變量。
區別總結:
- 用途不同:
- 方法參數的 out:用於指定輸出參數。
- 泛型的 out:用於指定類型參數的協變性。
- 位置不同:
- 方法參數的 out:用在方法參數列表中。
- 泛型的 out:用在泛型類型參數定義中。
- 功能不同:
- 方法參數的 out:確保方法將值賦給參數。
- 泛型的 out:允許更靈活的類型轉換和賦值。
- 編譯時行為: - 方法參數的 out:編譯器確保方法內部對參數賦值。 - 泛型的 out:編譯器允許特定的類型轉換。 所以, TryParse 方法中的 out 不是用於協變的,而是一個輸出參數標記。這兩種 out 雖然使用相同的關鍵字,但在 C# 中代表完全不同的概念。
簡單解釋共變數與反變數
共變數(Covariance)
定義:允許使用比原本指定的類別更具體(更繼承)的類型。 關鍵字:out 應用:通常用於返回類別或只讀泛型介面。
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 合法,因為 IEnumerable<T> 是共變數
反變數(Contravariance)
定義:允許使用比原本指定的類別更一般(更基礎)的類別。 關鍵字:in 應用:通常用於參數類別或只寫泛型介面。
Action<object> objectAction = (object obj) => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // 合法,因為 Action<T> 是反變數
詳細解釋和更多例子
共變數例子:
public class Animal { }
public class Dog : Animal { }
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // 共變數允許這種賦值
// 使用場景
void ProcessAnimals(IEnumerable<Animal> animals) { ... }
ProcessAnimals(dogs); // 可以傳入 Dog 的集合
反變數例子:
public interface IComparer<in T>
{
int Compare(T x, T y);
}
IComparer<Animal> animalComparer = new AnimalComparer();
IComparer<Dog> dogComparer = animalComparer; // 反變數允許這種賦值
// 使用場景
class AnimalComparer : IComparer<Animal>
{
public int Compare(Animal x, Animal y) { ... }
}
List<Dog> dogs = new List<Dog>();
dogs.Sort(dogComparer); // 可以使用 Animal 的比較器來比較 Dog
共變數和反變數的組合
public interface IConverter<in TInput, out TOutput>
{
TOutput Convert(TInput input);
}
IConverter<Animal, Dog> animalToDog = new AnimalToDogConverter();
IConverter<Dog, Animal> dogToAnimal = animalToDog; // 合法
限制和注意事項
- 共變數和反變數只適用於引用類型,不適用於值類型。
- 只能用於介面和委託的定義,不能用於類別。
- 一個類別參數不能同時標記為共變數和反變數。
- 共變數類別參數不能用作方法參數,反變數類別參數不能用作方法返回類型。
- 實際應用
- LINQ: LINQ 大量使用共變數和反變數來提高其靈活性。
- 事件處理:允許更靈活地處理不同類別的事件。
- 依賴注入:在依賴注入框架中常用,提高容器的靈活性。
- 性能考慮 共變數和反變數主要是編譯時特性,不會對運行時性能產生顯著影響。 總結: 共變數和反變數強大的特性,能夠提高程式碼的靈活性和可重用性。 共變數(out)允許使用更具體的類型,而反變數(in)允許使用更一般的類型。正確理解和使用這些概念可以幫助設計更靈活、更強大的 API 和框架。
static void Main(string[] args)
{
Dog dog = new Dog("doggy");
IOutDog<Dog> dogProvider = new DogProvider(dog);
IOutDog<Animal> animalProvider = dogProvider;
Console.WriteLine($"{animalProvider.Value.GetName()}, {dogProvider.Value.GetName()}");
// 使用基本的 AnimalSetter
IInDog<Animal> animalSetter = new AnimalSetter();
animalSetter.SetValue(new Animal());
animalSetter.SetValue(new Dog()); // 這也是有效的,因為 Dog 是 Animal 的子類
// 使用 DogSetter,展示反變數
// IInDog<Animal> dogSetterAsAnimal = (IInDog<Animal>)new DogSetter();
// dogSetterAsAnimal.SetValue(new Animal()); // 注意:這在運行時可能會拋出異常,因為實際的實現期望一個 Dog
// 使用泛型實現
IInDog<Dog> dogSetter = new GenericAnimalSetter<Dog>();
dogSetter.SetValue(new Dog());
IConverter<Animal, Dog> animalToDog = new AnimalToDogConverter();
IConverter<Dog, Animal> dogToAnimal = animalToDog; // 合法
}
public class Animal
{
private readonly string name;
public Animal(string name = "") => this.name = name;
public virtual string GetName() => name;
}
public class Dog : Animal
{
public Dog(string name = "") : base(name) { }
public override string GetName() => $"Dog {base.GetName()}";
}
public interface IOutDog<out T> where T : Animal
{
T Value { get; }
}
public class DogProvider : IOutDog<Dog>
{
public Dog Value { get; set; }
public DogProvider(Dog value) => Value = value;
}
public interface IInDog<in T> where T : Animal
{
void SetValue(T value);
}
public class AnimalSetter : IInDog<Animal>
{
private Animal value;
public void SetValue(Animal value) => this.value = value;
}
public class DogSetter : IInDog<Dog>
{
private Dog value;
public void SetValue(Dog value) => this.value = value;
}
public class GenericAnimalSetter<T> : IInDog<T> where T : Animal
{
private T value;
public void SetValue(T value) => this.value = value;
}
public interface IConverter<in TInput, out TOutput>
{
TOutput Convert(TInput input);
}
public class AnimalToDogConverter : IConverter<Animal, Dog>
{
public Dog Convert(Animal input) => new Dog(input.GetName());
}
心得
如果編譯器告訴你錯了 就要小心是不是有什麼地方搞錯了 做 cast 時要了解自己在做什麼 不然能編譯成功也會在 runtime 時炸掉
做法 23
使用 delegate 定義型別參數的方法約束
// double[] xValues = { 0,1,2,3,4,5,6,7,8,9,
// 0,1,2,3,4,5,6,7,8,9,};
// double[] yValues = { 0,1,2,3,4,5,6,7,8,9,
// 0,1,2,3,4,5,6,7,8,9,};
// List<Point> values = new List<Point>(Zip(xValues, yValues, (x, y) => new Point(x, y)));
private static IEnumerable<TOutput> Zip<T1, T2, TOutput>(IEnumerable<T1> left, IEnumerable<T2> right, Func<T1, T2, TOutput> generator)
{
IEnumerator<T1> leftEnumerator = left.GetEnumerator();
IEnumerator<T2> rightEnumerator = right.GetEnumerator();
while (leftEnumerator.MoveNext() && rightEnumerator.MoveNext())
{
yield return generator(leftEnumerator.Current, rightEnumerator.Current);
}
leftEnumerator.Dispose();
rightEnumerator.Dispose();
}
private class Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y)
{
X = x;
Y = y;
}
public Point(TextReader textReader) { }
}
/// InputCollection<Point> points = new InputCollection<Point>((InputStream) => new Point(InputStream));
public delegate T CreateFromStream<T>(System.IO.TextReader textReader);
public class InputCollection<T>
{
private List<T> thingsRead = new List<T>();
private readonly CreateFromStream<T> readFunc;
public InputCollection(CreateFromStream<T> readFunc)
{
this.readFunc = readFunc;
}
public void ReadFromStream(System.IO.TextReader textReader)
{
thingsRead.Add(readFunc(textReader));
}
public IEnumerable<T> Values => thingsRead;
}
做法 23 心得
可以利用 這種方式,讓不同類別擁有相同介面,使用相同的方法,讓程式彈性變高
// ForeachIn(items,(item, i) =>
// {
// item.SetIcon(icon[i]);
// });
// ForeachIn(items,(item, i) =>
// {
// item.SetPosition(positions[i]);
// });
public void ForeachIn<T>(T[] arr, Action<T, int> action)
{
for (int i = 0; i < arr.Length; i++)
{
action(arr[i], i);
}
}