【问题标题】:Partial generic type inference possible in C#?C# 中可能的部分泛型类型推断?
【发布时间】:2010-05-23 22:11:55
【问题描述】:

我正在为我的 IoC 类库重写我的流畅接口,当我重构一些代码以便通过基类共享一些通用功能时,我遇到了一个障碍。

注意:这是我想要做的事情,而不是我必须做的事情。如果我不得不使用不同的语法,我会的,但如果有人知道如何让我的代码按照我想要的方式编译,那将是非常受欢迎的。

我希望某些扩展方法可用于特定的基类,并且这些方法应该是泛型的,具有一个泛型类型,与方法的参数相关,但方法还应该返回与方法相关的特定类型调用它们的特定后代。

使用代码示例比上面的描述更好。

这是一个简单而完整的例子,说明不起作用

using System;

namespace ConsoleApplication16
{
    public class ParameterizedRegistrationBase { }
    public class ConcreteTypeRegistration : ParameterizedRegistrationBase
    {
        public void SomethingConcrete() { }
    }
    public class DelegateRegistration : ParameterizedRegistrationBase
    {
        public void SomethingDelegated() { }
    }

    public static class Extensions
    {
        public static ParameterizedRegistrationBase Parameter<T>(
            this ParameterizedRegistrationBase p, string name, T value)
        {
            return p;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ConcreteTypeRegistration ct = new ConcreteTypeRegistration();
            ct
                .Parameter<int>("age", 20)
                .SomethingConcrete(); // <-- this is not available

            DelegateRegistration del = new DelegateRegistration();
            del
                .Parameter<int>("age", 20)
                .SomethingDelegated(); // <-- neither is this
        }
    }
}

如果你编译这个,你会得到:

'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingConcrete' and no extension method 'SomethingConcrete'...
'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingDelegated' and no extension method 'SomethingDelegated'...

我想要的是扩展方法 (Parameter&lt;T&gt;) 能够在 ConcreteTypeRegistrationDelegateRegistration 上调用,并且在这两种情况下,返回类型都应该与调用扩展的类型相匹配。

问题如下:

我想写:

ct.Parameter<string>("name", "Lasse")
            ^------^
            notice only one generic argument

而且Parameter&lt;T&gt; 返回一个与调用它的类型相同的对象,这意味着:

ct.Parameter<string>("name", "Lasse").SomethingConcrete();
^                                     ^-------+-------^
|                                             |
+---------------------------------------------+
   .SomethingConcrete comes from the object in "ct"
   which in this case is of type ConcreteTypeRegistration

有什么方法可以欺骗编译器为我实现这一飞跃?

如果我将两个泛型类型参数添加到 Parameter 方法,类型推断会迫使我要么提供两者,要么不提供,这意味着:

public static TReg Parameter<TReg, T>(
    this TReg p, string name, T value)
    where TReg : ParameterizedRegistrationBase

给我这个:

Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments
Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments

这同样糟糕。

我可以轻松地重组类,甚至通过将它们引入层次结构来使方法成为非扩展方法,但我的问题是我是否可以避免为两个后代复制方法,并以某种方式声明它们只有一次,用于基类。

让我换个说法。有没有办法更改上面第一个代码示例中的类,以便保留 Main-method 中的语法,而无需复制相关方法?

代码必须与 C# 3.0 和 4.0 兼容。


编辑:我不想将两个泛型类型参数都留给推理的原因是,对于某些服务,我想为一个类型的构造函数参数指定一个参数值,但是传入一个后代值。目前,指定参数值和要调用的正确构造函数的匹配是使用参数的名称和类型完成的。

我举个例子:

ServiceContainerBuilder.Register<ISomeService>(r => r
    .From(f => f.ConcreteType<FileService>(ct => ct
        .Parameter<Stream>("source", new FileStream(...)))));
                  ^--+---^               ^---+----^
                     |                       |
                     |                       +- has to be a descendant of Stream
                     |
                     +- has to match constructor of FileService

如果我让两者都进行类型推断,则参数类型将是 FileStream,而不是 Stream

【问题讨论】:

    标签: c# generics type-inference fluent-interface


    【解决方案1】:

    我想创建一个扩展方法,可以枚举事物列表,并返回特定类型的事物列表。它看起来像这样:

    listOfFruits.ThatAre<Banana>().Where(banana => banana.Peel != Color.Black) ...
    

    很遗憾,这是不可能的。此扩展方法的建议签名如下所示:

    public static IEnumerable<TResult> ThatAre<TSource, TResult>
        (this IEnumerable<TSource> source) where TResult : TSource
    

    ... 并且对 ThatAre 的调用失败,因为需要指定两个类型参数,即使 TSource 可以从用法中推断出来。

    按照其他答案中的建议,我创建了两个函数:一个捕获源代码,另一个允许调用者表达结果:

    public static ThatAreWrapper<TSource> That<TSource>
        (this IEnumerable<TSource> source)
    {
        return new ThatAreWrapper<TSource>(source);
    }
    
    public class ThatAreWrapper<TSource>
    {
        private readonly IEnumerable<TSource> SourceCollection;
        public ThatAreWrapper(IEnumerable<TSource> source)
        {
            SourceCollection = source;
        }
        public IEnumerable<TResult> Are<TResult>() where TResult : TSource
        {
            foreach (var sourceItem in SourceCollection)
                if (sourceItem is TResult) yield return (TResult)sourceItem;
            }
        }
    }
    

    这会产生以下调用代码:

    listOfFruits.That().Are<Banana>().Where(banana => banana.Peel != Color.Black) ...
    

    ...这还不错。

    请注意,由于泛型类型的限制,下面的代码:

    listOfFruits.That().Are<Truck>().Where(truck => truck.Horn.IsBroken) ...
    

    将无法在 Are() 步骤编译,因为卡车不是水果。这胜过提供的 .OfType 函数:

    listOfFruits.OfType<Truck>().Where(truck => truck.Horn.IsBroken) ...
    

    这可以编译,但总是产生零结果,而且尝试确实没有任何意义。让编译器帮你发现这些东西会更好。

    【讨论】:

    • 我想你和上面的 Thomas 说的一样,但我更理解你的回答。
    • 出于好奇,你为什么不使用.OfType&lt;T&gt;()
    • @recursive:因为 .OfType 没有强制您尝试转换为的类型必须与源兼容的限制。编辑了回复,最后一段。
    • 不知何故,早在 2011 年我就没有注意到这一点。
    • +1 用于在编译时捕获错误!我可以看到 OfType 在某些情况下很有用,但我认为您尽早清除不兼容类的方法更胜一筹。
    【解决方案2】:

    如果您只有两种特定类型的注册(您的问题似乎就是这种情况),您可以简单地实现两种扩展方法:

    public static DelegateRegistration Parameter<T>( 
       this DelegateRegistration p, string name, T value); 
    
    public static ConcreteTypeRegistration Parameter<T>( 
       this ConcreteTypeRegistration p, string name, T value); 
    

    那么您不需要指定类型参数,因此类型推断将在您提到的示例中起作用。请注意,您可以通过委托给具有两个类型参数(您的问题中的那个)的单个通用扩展方法来实现这两种扩展方法。


    一般来说,C# 不支持像 o.Foo&lt;int, ?&gt;(..) 这样的东西来仅推断第二个类型参数(这将是一个不错的功能 - F# 有它并且非常有用:-))。您可能可以实现一种解决方法,允许您编写此代码(基本上,通过将调用分为两个方法调用,以获得可以应用类型推断的两个位置):

    FooTrick<int>().Apply(); // where Apply is a generic method
    

    这是一个演示结构的伪代码:

    // in the original object
    FooImmediateWrapper<T> FooTrick<T>() { 
      return new FooImmediateWrapper<T> { InvokeOn = this; } 
    }
    // in the FooImmediateWrapper<T> class
    (...) Apply<R>(arguments) { 
      this.InvokeOn.Foo<T, R>(arguments);
    }
    

    【讨论】:

    • 是的,这看起来是最好的解决方案。我现在正在测试一个,我也将泛型引入了基类,并且只是删除了扩展方法,但是您的示例让我将它们保留为扩展方法,在这种情况下我喜欢这样做。也就是说,两个扩展方法只调用一个指定正确类型的通用方法。
    【解决方案3】:

    为什么不指定零类型参数?两者都可以在您的样本中推断出来。如果这对您来说不是一个可接受的解决方案,我也经常遇到这个问题,并且没有简单的方法来解决“仅推断一个类型参数”的问题。所以我会使用重复的方法。

    【讨论】:

    • 我宁愿避免这种情况,在将具体类型注册到我的 IoC 实现时,我可以为构造函数参数提供参数值,并且我必须指定参数的名称及其类型,然后还提供一个传递给它的值。指定的类型必须匹配,但传递的实际值可以是后代。例如:.Parameter&lt;Stream&gt;("source", new FileStream(...))。在这里,如果我将两者都留给推理,则参数的类型将是FileStream,因此我必须实现后代的匹配。虽然我可以这样做,但我宁愿不这样做。
    【解决方案4】:

    下面的呢:

    使用您提供的定义: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase

    然后转换参数,以便推理引擎得到正确的类型:

    ServiceContainerBuilder.Register<ISomeService>(r => r
    .From(f => f.ConcreteType<FileService>(ct => ct
        .Parameter("source", (Stream)new FileStream(...)))));
    

    【讨论】:

      【解决方案5】:

      我认为您需要在两个不同的表达式之间拆分两个类型参数;使显式的成为扩展方法的参数类型的一部分,因此推断可以将其拾取。

      假设你声明了一个包装类:

      public class TypedValue<TValue>
      {
          public TypedValue(TValue value)
          {
              Value = value;
          }
      
          public TValue Value { get; private set; }
      }
      

      那么你的扩展方法为:

      public static class Extensions
      {
          public static TReg Parameter<TValue, TReg>(
              this TReg p, string name, TypedValue<TValue> value) 
              where TReg : ParameterizedRegistrationBase
          {
              // can get at value.Value
              return p;
          }
      }
      

      加上一个更简单的重载(上面实际上可以称之为这个):

      public static class Extensions
      {
          public static TReg Parameter<TValue, TReg>(
              this TReg p, string name, TValue value) 
              where TReg : ParameterizedRegistrationBase
          {
              return p;
          }
      }
      

      现在在您很乐意推断参数值类型的简单情况下:

      ct.Parameter("name", "Lasse")
      

      但在需要显式声明类型的情况下,可以这样做:

      ct.Parameter("list", new TypedValue<IEnumerable<int>>(new List<int>()))
      

      看起来很难看,但希望比简单的完全推断类型更罕见。

      请注意,您可以只使用无包装器重载并编写:

      ct.Parameter("list", (IEnumerable<int>)(new List<int>()))
      

      但这当然有一个缺点,如果你出错了,就会在运行时失败。不幸的是,我现在离开了我的 C# 编译器,如果这太远了,请道歉。

      【讨论】:

        【解决方案6】:

        我会使用解决方案:

        public class JsonDictionary
        {
            public static readonly Key<int> Foo = new Key<int> { Name = "FOO" };
            public static readonly Key<string> Bar = new Key<string> { Name = "BAR" };
                
            IDictionary<string, object> _data;
            public JsonDictionary()
            {
                _data = new Dictionary<string, object>();
            }
            
            public void Set<T>(Key<T> key, T obj)
            {
                _data[key.Name] = obj;
            }
        
            public T Get<T>(Key<T> key)
            {
                return (T)_data[key.Name];
            }
            
            public class Key<T>
            {
                public string Name { get; init; }
            }
        }
        

        见:

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多