繁体   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