[英]C# generic interface specialization
我想知道是否有可能以某種方式在 C# 中以某種方式專門化通用接口方法? 我發現了類似的問題,但沒有完全像這樣。 現在我懷疑答案是“不,你不能”,但我想確認一下。
我所擁有的類似於以下內容。
public interface IStorage
{
void Store<T>(T data);
}
public class Storage : IStorage
{
public void Store<T>(T data)
{
Console.WriteLine("Generic");
}
public void Store(int data)
{
Console.WriteLine("Specific");
}
}
class Program
{
static void Main(string[] args)
{
IStorage i = new Storage();
i.Store("somestring"); // Prints Generic
i.Store(1); // Prints Generic
Storage s = (Storage)i;
s.Store("somestring"); // Prints Generic
s.Store(1); // Prints Specific
}
}
當通過接口調用時,有沒有辦法讓它使用專門版本的 Store? 如果沒有,有人知道 C# 以這種方式處理泛型參數的確切原因嗎?
編輯:如果不是這樣,C# 無法在多個步驟中解析模板參數,則可以解決該問題。
void Foo<T>(T t)
{
SubFoo(t);
}
void SubFoo<T>(T t)
{
Console.WriteLine("Generic");
}
void SubFoo(int t)
{
Console.WriteLine("Specific");
}
在這里調用 Foo(1) 也會打印“Generic”,編譯器不應該能夠解決這個問題嗎? 或者 JIT 是否阻止了這種情況?
重載解析在編譯時執行,而不是在運行時根據傳遞值的實際類型執行。
IStorage i = new Storage();
i.Store("somestring"); // Prints Generic
i.Store(1); // Prints Generic
這將始終調用“通用”方法,因為IStorage
只有一個Store
重載並且編譯器不知道i
實際上包含一個Storage
對象。 編譯器如何知道Storage
的其他重載?
Storage s = (Storage)i;
s.Store("somestring"); // Prints Generic
s.Store(1); // Prints Specific
在這里,編譯器知道s
包含一個Storage
對象(或一個從Storage
派生的對象),因為s
是這樣聲明的。 所以它看到了兩個重載。 它為int
值選擇特定的重載,因為重載解析規則說比通用重載更喜歡特定的重載。
在運行時確定泛型方法中的typeof(T)
並將方法調用轉發到特定方法在技術上是可能的。 但如果你仔細想想,這並沒有多大意義。 泛型方法意味着相同的實現適用於不同的、不相關的類型的參數。 如果您想要不同類型的不同實現,則不應為此使用泛型。
void Foo<T>(T t)
{
SubFoo(t);
}
void SubFoo<T>(T t);
void SubFoo(int t);
泛型的工作方式與 C++ 模板有很大不同。 C# 編譯器只將 Foo 編譯一次——編譯成一個泛型方法。 請記住:泛型意味着不同類型的相同實現。 C# 編譯器在編譯時不知道 T 是int
還是string
或任何其他類型。 因此,適用於任何 T 的 Foo 的唯一可能實現是調用 SubFoo<T>。 如果根據 T 調用 SubFoo 重載之一,則所有 T 的 Foo 實現將不再相同。
為什么基於通用代碼的專業化在現實世界中很有意義,特別是在擴展方法中?
我將舉一個關於集合的例子,因為每個人都或多或少知道 .NET 集合。
我將采用.Last(this IEnumerable<<T>> coll)
擴展方法的簡單示例。 在 .NET Framework 中,此方法使用代碼內類型特化。
首先,關於類型特化的好處,這個例子很清楚。 一些可枚舉的集合需要掃描整個集合並返回最后一個元素,基於數組的只需要返回數組的最后一個分配元素,許多鏈表都有一個指向最后一個元素的指針......所以實現了具有類型特化的泛型可以使.Last()
方法效率更高。
其次,因為這個方法是靜態的,每個集合類型或接口都有很多實現並不能解決正確方法選擇的問題。 實際上,正確方法的選擇是在編譯時根據 coll 對象的明顯類型完成的。 如果你想像,你想在List<<T>>
上應用連續的擴展方法,第一個可能不需要每個集合類型的許多特殊實現,並使用一個基於IEnumerable<<T>>
。 所以即使我們有一個.Last(this List<<T>> coll)
,第一個非專用擴展方法將返回一個IEnumerable<<T>>
並且專用的.Last(this List<<T>> coll)
將不能用於List<<T>>
。
因此,如果您的代碼使用外部程序集(甚至 .NET Framework 本身),如果您必須在兩周內為復雜的體系結構問題提供解決方案……您將離開完美領域進入現實世界。 泛型類型特化成為一個不容忽視的選擇。
如果您想利用編譯時重載解析的優勢,您也可以使用采用int
的方法擴展接口:
public interface IStorage
{
void Store<T>(T data);
}
public interface IIntStorage: IStorage
{
void Store(int data);
}
public class Storage : IIntStorage
{
public void Store<T>(T data)
{
Console.WriteLine("Generic");
}
public void Store(int data)
{
Console.WriteLine("Specific");
}
}
現在,如果您通過IIntStorage
接口調用Store(1)
它將使用專門的方法(類似於您直接調用Storage
的方法的方式),但是如果您通過IStorage
調用它,它將仍然使用通用版本。
你可以這樣做:
public interface IStorage<T>
{
void Store(object data);
void Store<T>(T data);
}
public class Storage : IStorage<int>
{
public void Store(object data)
{
Console.WriteLine("Generic");
}
public void Store(int data)
{
Console.WriteLine("Specific");
}
}
您已將 i 鍵入為 IStorage 並且該接口未定義重載的 Store 方法。
您可以通過引入其他類型信息(例如實現接口)來實現。 這是一個例子。
// no need to modify the original interface
public interface IStorage
{
void Store<T>(T data);
}
基於通用接口的專業化實現
public class Storage : IStorage,
Storage.ISpecializedFor<int>,
Storage.ISpecializedFor<double>
{
// a private interface to provide additional type info
private interface ISpecializedFor<T>
{
void Store(T data);
}
public void Store<T>(T data)
{
// resolve specialization
if (this is ISpecializedFor<T> specialized)
{
specialized.Store(data);
}
else
{
// unspecialized implementation goes here
Console.WriteLine("Unspecialized");
}
}
// specialized implementation
void ISpecializedFor<int>.Store(int data)
{
Console.WriteLine("Specialized for int");
}
void ISpecializedFor<double>.Store(double data)
{
Console.WriteLine("Specialized for double");
}
}
結果
void Main()
{
IStorage storage = new Storage();
storage.Store("hello"); // prints "Unspecialized"
storage.Store(42); // prints "Specialized for int"
storage.Store(1.0); // prints "Specialized for double"
}
由於 C# 泛型在某些情況下是運行時模板,因此您應該使用運行時特化。 例如,在通用靜態方法中,繼承和接口不可用。 如果您想專門化通用靜態方法 - 特別是擴展方法 - 您必須使用以下結構檢測代碼中的類型:
如果 (typeof(T)==typeof(bool))
對於引用類型(例如字符串)和參數 T 數據的特化,您更喜歡:
字符串 s = 數據作為字符串; 如果(s!=空)
在這個例子中,一個問題來自於特殊代碼中 T 和 bool 之間的轉換:您知道 T 是 bool 但語言不允許在這些類型之間進行轉換。 解決方案來自對象類型:可以將對象強制轉換為任何類型(在這種情況下,在運行時而不是在編譯時檢查轉換)。 所以如果你有
數據;
你可以寫:
bool b=(bool)(object)data; 數據=(T)(對象)b;
這並不完美:如果類型相等性非常快,在某些情況下,您必須測試 T 是否是指定類型的派生類型(稍長一些)。 當 T 是像 bool 這樣的值類型時,轉換為對象,然后返回到類型平均值類型裝箱/拆箱和運行時類型檢查引用類型。 運行時優化器可以刪除這些不必要的步驟,但我不能說他們是否這樣做了。
根據您的靜態方法的使用情況,請記住您可以應用 where T: ... 對參數化類型的限制。 並且 default(T) 對於布爾值返回 false,對於數字基類型返回零,對於引用類型返回 null。
運行時專業化意味着額外的測試步驟和裝箱/拆箱/運行時類型檢查,因此它不是萬能葯,但在許多情況下允許在可接受的時間內過於專業化泛型方法:對於長時間操作(特別是優化)或隱藏或類型分組復雜性管理比性能更重要。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.