Effective C# 23.使用 delegate 定義型別參數的方法約束 Effective C# 23.使用 delegate 定義型別參數的方法約束 (Use Delegates to Define Method Constraints on Type Parameters)

Published on Saturday, October 19, 2024

在做法 18 中提出了約束的概念,用意是約束傳入的類型參數(type parameter)需要符合設定的條件才允使傳入到泛型之中,但是內建的約束並不是 萬能的,像是 new() 這個約束就要求類型參數需要實做無參數的建構函式,如果今天想要約束類型參數需要實做靜態的建構函式或者需要強制實做一個 方法就需要特殊處理了。

假設我有一個 IAdd<T> 介面並且包含一個 Add 方法。

public interface IAdd<T>
{
	public T Add(T t1, T t2);
}

今天我想要寫一個泛型類型並且約束 T2 需要實做 IAdd<T1> 並且實例化後使用 Add 方法。

public class Temp<T1, T2> where T2 : IAdd<T1>, new()
{
	public T1 Add(T1 t1, T1 t2)
	{
		return new T2().Add(t1, t2);
	}
}

對於使用我們泛型類別的使用者來說,他們需要先實例化 IAdd<T> 介面並實做 Add 方法,還要提供類別將 Temp<T1, T2> 這個開放泛型 定義成封閉泛型最後才能使用泛型方法。

public class MyTemp : Temp<int, MyAdd>
{

}

public class MyAdd : IAdd<int>
{
	public int Add(int t1, int t2)
	{
		return t1 + t2;
	}
}

void Main()
{
	var m = new MyTemp();
	Console.WriteLine(m.Add(1, 2));
}

在這種情況下可以使用本做法提到的直接用 delegate 定義方法約束能夠省下許多程式碼,下面這段程式碼把實做交給使用者進行決定, Func 可以使用舊的寫法傳入也可以用 lambda 語法。

void Main()
{
	int a = 8;
	int b = 1;
	Console.WriteLine(Temp.Add(a, b, MyAdd));
	Console.WriteLine(Temp.Add(a, b, (x, y) => x + y));
}

public int MyAdd(int x, int y)
{
	return x + y;
}

public class Temp
{
	public static T3 Add<T1, T2, T3>(T1 t1, T2 t2, Func<T1, T2, T3> AddFunc)
	{
		return AddFunc(t1, t2);
	}
}

下面這個範例建立一個 InputCollection<T> 泛型類型,我們可以傳入 TextReader 將讀取到的字串新增到 List 裡面。

void Main()
{
	var readValues = new InputCollection<Point>(
   (inputStream) => new Point(inputStream));

	using (TextReader reader = new StringReader("1,2"))
	{
		readValues.ReadFromStream(reader);
	}
	
	Console.WriteLine(readValues);
}
public delegate T CreateFromStream<T>(TextReader reader);

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(TextReader reader) =>
		thingsRead.Add(readFunc(reader));
	public IEnumerable<T> Values => thingsRead;
}

public class Point
{
	public double X { get; }
	public double Y { get; }

	public Point(double x, double y)
	{
		this.X = x;
		this.Y = y;
	}
	
	public Point(TextReader reader)
	{
		string line = reader.ReadLine();
		string[] fields = line.Split(',');
		if (fields.Length != 2)
			throw new InvalidOperationException("Input format incorrect");
		double value;
		if (!double.TryParse(fields[0], out value))
			throw new InvalidOperationException("Could not parse X value");
		else
			X = value;
		if (!double.TryParse(fields[1], out value))
			throw new InvalidOperationException("Could not parse Y value");
		else
			Y = value;
	}
}

Summary

這個做法建議用委派的方式來建立方法約束,會比寫一個介面來說方便許多,並且寫起來也比較方便好懂。