這個做法說明最好避免在任何公開的方法使用 dynamic 物件來避免蔓延,還有建議限制 dynamic 的使用範圍避免它把所有的物件變成動態物件。

之前有提到 dynamic 物件會跳過某些編譯檢查,並且它會把解析型別的過程延後到執行時期,這個流程等於就放棄了靜態程式語言提供的型別安全性, 還有如果你的方法回傳的是 dynamic 物件就很可能會導致它蔓延到四處各地,例如下面這個方法可以傳入兩個 dynamic 物件並回傳新的 dynamic 物件, 如果下一步還要額外操作可能又要額外的 dynamic 方法來處理這個回傳值。

public dynamic Add(dynamic x, dynamic y)
{
	return x + y;
}

並且編譯器為了處理動態型別必須產生非常多的程式碼才能讓動態型別能夠在執行時期解析出真正的型別,同時每執行一次就必須重新解析一次 這樣會會對應用程式的性能造成影響,例如下面這麼簡單的呼叫背後其實產生大量的輔助程式碼。

void Main()
{
	dynamic answer = Add(5, 5);
	Console.WriteLine(answer);
}

public dynamic Add(dynamic x, dynamic y)
{
	return x + y;
}
[CompilerGenerated]
private static class <>o__0
{
    public static CallSite<Action<CallSite, Type, object>> <>p__0;
}


[CompilerGenerated]
private static class <>o__1
{
    public static CallSite<Func<CallSite, object, object, object>> <>p__0;
}

public void M()
{
    object arg = Add(5, 5);
    if (<>o__0.<>p__0 == null)
    {
        Type typeFromHandle = typeof(C);
        CSharpArgumentInfo[] array = new CSharpArgumentInfo[2];
        array[0] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, null);
        array[1] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
        <>o__0.<>p__0 = CallSite<Action<CallSite, Type, object>>.Create(Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "WriteLine", null, typeFromHandle, array));
    }
    <>o__0.<>p__0.Target(<>o__0.<>p__0, typeof(Console), arg);
}

[NullableContext(1)]
[return: Dynamic]
public object Add([Dynamic] object x, [Dynamic] object y)
{
    if (<>o__1.<>p__0 == null)
    {
        Type typeFromHandle = typeof(C);
        CSharpArgumentInfo[] array = new CSharpArgumentInfo[2];
        array[0] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
        array[1] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
        <>o__1.<>p__0 = CallSite<Func<CallSite, object, object, object>>.Create(Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Add, typeFromHandle, array));
    }
    return <>o__1.<>p__0.Target(<>o__1.<>p__0, x, y);
}

我們可以透過一個泛型的 Add 方法,將動態的方法包覆起來這樣就能不直接呼叫動態的方法,這樣能避免編譯器產生非常多相同輔助的程式碼, 提供泛型的版本就能把這些輔助程式碼限制在這一個範圍內。

private static dynamic DynamicAdd(dynamic left, dynamic right) => left + right;
public static T1 Add<T1, T2>(T1 left, T2 right)
{
	dynamic result = DynamicAdd(left, right);
	return (T1)result;
}

這種處理方式能讓呼叫端繼續保持強型別,同時又能保持動態方法的特性,例如下面這段寫法背後還是 dynamic 所以能同時支援多個型別, 並且回傳值也不會變成動態,確保動態型別不會蔓延。

void Main()
{
	var answer = Add(5, 5);
	Console.WriteLine(answer);

	DateTime tomorrow = Add(DateTime.Now, TimeSpan.FromDays(1));
	Console.WriteLine(tomorrow);
}

另外涉及到動態資料的場合,例如 CSV 解析,也適合用 dynamic 來處理,因為實際的欄位名稱無法在編譯時期確定, 這個例子只有對外開放兩個公開成員 this[int index]Rows 這裡公開 dynamic 物件給外部使用雖然不是很好的設計, 但因為是必要的所以算是可接受的設計。

void Main()
{
	var csv = @"Name,Age
	John,30
	Jane,25";

	using var reader = new StringReader(csv);
	var container = new CSVDataContainer(reader);
}

public class CSVDataContainer
{
	private class CSVRow : DynamicObject
	{
		private List<(string, string)> values = new List<(string, string)>();
		public CSVRow(IEnumerable<string> headers, IEnumerable<string> items)
		{
			values.AddRange(headers.Zip(items, (header, value) => 
				(header,value))
			);
		}
		public override bool TryGetMember(GetMemberBinder binder, out object result)
		{
			var answer = values.FirstOrDefault(n => n.Item1 == binder.Name);
			result = answer.Item2;
			return result != null;
		}
	}

	private List<string> columnNames = new List<string>();
	private List<CSVRow> data = new List<CSVRow>();
	public CSVDataContainer(TextReader stream)
	{
		// read headers:
		var headers = stream.ReadLine();
		columnNames = (from header in headers.Split(',')
			 select header.Trim()).ToList();
		var line = stream.ReadLine();
		while (line != null)
		{
			var items = line.Split(',');
			data.Add(new CSVRow(columnNames, items));
			line = stream.ReadLine();
		}
	}
	public dynamic this[int index] => data[index];
	public IEnumerable<dynamic> Rows => data;
}

Summary

使用 dynamic 的物件時首先需要避免過度蔓延,主要可以透過額外的泛型方法來限制範圍,另外只有在必要的時刻才使用 dynamic 例如 csv 與 json 這種 需要處理動態資料的場合,同時還要注意處理完後的資料型別,如果是 dynamic 則要思考清楚開放這個成員是不是必要的。