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