簡體   English   中英

C#泛型接口特化

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM