【问题标题】:C# generic interface specializationC# 泛型接口特化
【发布时间】:2020-08-10 10:27:16
【问题描述】:

我想知道是否可以在 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 会阻止这种情况发生吗?

【问题讨论】:

标签: c#


【解决方案1】:

重载解析在编译时执行,而不是在运行时基于传递值的实际类型。

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 调用 SubFoo 重载之一,则所有 T 的 Foo 实现将不再相同。

【讨论】:

  • 通用 T 的意义何在?为什么不直接将 data 参数作为对象输入并测试 if (data is int)?
  • 从编译器的角度考虑时,这很有意义。虽然我有一个模糊的记忆,但 C++ 可以以某种方式完成此任务,尽管我现在无法让它工作。我将稍微编辑我的问题以解释阻止解决此问题的进一步问题,我希望您也可以解释这一点,尽管我会接受这个答案:)
  • 我希望它的工作方式与 C++ 模板更相似,因为有关对 Foo 的调用的信息在编译时可用,我希望它在传递一个 int 时调用 Foo 并且Foo 当一个字符串传递时,它会依次调用 SubFoo(someInt) 和 SubFoo(someString) 就像 C++ 做的 C++ 模板一样,编译时也是如此。
  • Jamie:这样想你也可以废弃泛型,因为你总是可以用对象和类型转换来替换它。但随后你放弃了类型安全,你也会损失性能。
【解决方案2】:

为什么基于通用代码的专业化在现实世界中非常有意义,尤其是在扩展方法中?

我将举一个关于集合的例子,因为每个人都或多或少地知道 .NET 集合。

我将以.Last(this IEnumerable&lt;&lt;T&gt;&gt; coll) 扩展方法的简单示例为例。在 .NET Framework 中,此方法使用代码内类型特化。

首先,关于类型特化的好处,这个例子很清楚。一些可枚举的集合需要扫描整个集合并返回最后一个元素,基于数组的集合只需要返回数组的最后一个分配的元素,许多链表都有一个指向最后一个元素的指针......所以实现一个具有类型特化的泛型可以使.Last() 方法更加高效。

第二,因为这个方法是静态的,每个集合类型或接口都有很多实现并不能解决正确方法选择的问题。实际上,正确方法的选择是在编译时根据 coll 对象的明显类型完成的。如果您想像,您想在List&lt;&lt;T&gt;&gt; 上应用连续的扩展方法,第一个可能不需要针对每个集合类型的许多专门实现,而是使用基于IEnumerable&lt;&lt;T&gt;&gt; 的单个扩展方法。因此,即使我们有一个.Last(this List&lt;&lt;T&gt;&gt; coll),第一个非专用扩展方法也会返回一个IEnumerable&lt;&lt;T&gt;&gt;,而专用的.Last(this List&lt;&lt;T&gt;&gt; coll) 将不会用于List&lt;&lt;T&gt;&gt;

因此,如果您的代码使用外部程序集(甚至是 .NET Framework 本身),如果您必须在两周内为复杂的架构问题提供解决方案……您就离开了完美的领域,进入了现实世界。并且泛型类型特化成为一个不容忽视的选择。

【讨论】:

    【解决方案3】:

    如果您想利用编译时重载解析,您还可以使用采用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调用它仍然会使用通用版本。

    【讨论】:

    • 我想要一个通用接口,而不是每种类型的专用类 :)
    【解决方案4】:

    你可以这样做:

    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 方法。

    【讨论】:

      【解决方案5】:

      您可以通过引入额外的类型信息来做到这一点(例如,实现一个接口)。这是一个例子。

      // 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"
      }
      

      【讨论】:

        【解决方案6】:

        因为 C# 泛型在某些情况下是运行时模板,所以您应该使用运行时特化。例如,在泛型静态方法中,继承和接口是不可用的。如果您想专门化通用静态方法 - 特别是扩展方法 - 您必须使用以下结构检测代码中的类型:

        if (typeof(T)==typeof(bool))

        对于引用类型的特化(例如字符串)和参数 T 数据,您更喜欢:

        string s = 数据作为字符串; 如果 (s!=null)

        在这个例子中,一个问题来自于 T 和 bool 在特殊代码中的转换:你知道 T 是 bool,但是语言不允许这些类型之间的转换。解决方案来自对象类型:对象可以转换为任何类型(在这种情况下,在运行时而不是在编译时检查转换)。所以如果你有

        T 数据;

        你可以写:

        bool b=(bool)(对象)数据; 数据=(T)(对象)b;

        这并不完美:如果类型相等很快,在某些情况下,您必须测试 T 是否是指定类型的派生类型(稍长一些)。当 T 是像 bool 这样的值类型时,转换为对象,然后返回到类型平均值类型的装箱/拆箱和运行时类型检查引用类型。运行时优化器可以删除这些不必要的步骤,但我不能说他们是否这样做。

        根据您的静态方法的使用情况,请记住您可以应用 where T: ... 对参数化类型的限制。并且该 default(T) 对于布尔值返回 false,对于数字基本类型返回零,对于引用类型返回 null。

        运行时专业化意味着额外的测试步骤和装箱/拆箱/运行时类型检查,因此它不是万能药,但在许多情况下允许在可接受的时间内过于专业化通用方法:对于长时间操作(特别是优化)或当隐藏或分组类型时,复杂性管理比性能更重要。

        【讨论】:

        • 在从T 转换或检查T 实例的类型之前,最好检查T 本身的类型。如果T 是不可为空的值类型,则T 类型的存储位置将保存T 类型的实例;在存储位置调用 GetType() 需要对它进行装箱,但检查 T 的类型则不需要。请注意,即使T 不是值类型,T 类型的存储位置也可能包含对装箱值类型实例的引用。
        猜你喜欢
        • 1970-01-01
        • 2014-08-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多