简体   繁体   English

C# 中可以进行部分泛型类型推断吗?

[英]Partial generic type inference possible in C#?

I am working on rewriting my fluent interface for my IoC class library, and when I refactored some code in order to share some common functionality through a base class, I hit upon a snag.我正在为我的 IoC 类库重写我的流畅接口,当我重构一些代码以通过基类共享一些通用功能时,我遇到了一个障碍。

Note : This is something I want to do, not something I have to do.注意:这是我想做的事情,而不是我必须做的事情。 If I have to make do with a different syntax, I will, but if anyone has an idea on how to make my code compile the way I want it, it would be most welcome.如果我不得不使用不同的语法,我会这样做,但是如果有人知道如何让我的代码按照我想要的方式编译,那将是最受欢迎的。

I want some extension methods to be available for a specific base-class, and these methods should be generic, with one generic type, related to an argument to the method, but the methods should also return a specific type related to the particular descendant they're invoked upon.我希望一些扩展方法可用于特定的基类,并且这些方法应该是通用的,具有一个通用类型,与方法的参数相关,但这些方法还应该返回与它们的特定后代相关的特定类型'被调用。

Better with a code example than the above description methinks.使用代码示例比上面的描述更好。

Here's a simple and complete example of what doesn't work:这是一个简单而完整的示例,说明什么不起作用

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

If you compile this, you'll get:如果你编译这个,你会得到:

'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'...

What I want is for the extension method ( Parameter<T> ) to be able to be invoked on both ConcreteTypeRegistration and DelegateRegistration , and in both cases the return type should match the type the extension was invoked on.我想要的是扩展方法( Parameter<T> )能够在ConcreteTypeRegistrationDelegateRegistration上调用,并且在这两种情况下,返回类型都应该与调用扩展的类型相匹配。

The problem is as follows:问题如下:

I would like to write:我想写:

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

but also that Parameter<T> returns an object of the same type it was invoked on, which means:而且Parameter<T>返回一个与它被调用的类型相同的对象,这意味着:

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

Is there any way I can trick the compiler into making this leap for me?有什么办法可以欺骗编译器为我实现这一飞跃吗?

If I add two generic type arguments to the Parameter method, type inference forces me to either provide both, or none, which means this:如果我向Parameter方法添加两个泛型类型参数,类型推断会强制我要么提供两者,要么不提供,这意味着:

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

gives me this:给我这个:

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

Which is just as bad.这同样糟糕。

I can easily restructure the classes, or even make the methods non-extension-methods by introducing them into the hierarchy, but my question is if I can avoid having to duplicate the methods for the two descendants, and in some way declare them only once, for the base class.我可以轻松地重组类,甚至可以通过将它们引入层次结构来使方法成为非扩展方法,但我的问题是我是否可以避免为两个后代重复方法,并以某种方式只声明一次,为基类。

Let me rephrase that.让我重新表述一下。 Is there a way to change the classes in the first code example above, so that the syntax in the Main-method can be kept, without duplicating the methods in question?有没有办法更改上面第一个代码示例中的类,以便可以保留 Main-method 中的语法,而不会复制有问题的方法?

The code will have to be compatible with both C# 3.0 and 4.0.代码必须与 C# 3.0 和 4.0 兼容。


Edit : The reason I'd rather not leave both generic type arguments to inference is that for some services, I want to specify a parameter value for a constructor parameter that is of one type, but pass in a value that is a descendant.编辑:我不想将两个泛型类型参数都留给推理的原因是,对于某些服务,我想为一种类型的构造函数参数指定一个参数值,但传入一个后代值。 For the moment, matching of specified argument values and the correct constructor to call is done using both the name and the type of the argument.目前,指定参数值和要调用的正确构造函数的匹配是使用参数的名称和类型完成的。

Let me give an example:让我举个例子吧:

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

If I leave both to type inference, the parameter type will be FileStream , not Stream .如果我让两者都进行类型推断,则参数类型将是FileStream ,而不是Stream

I wanted to create an extension method that could enumerate over a list of things, and return a list of those things that were of a certain type.我想创建一个扩展方法,它可以枚举事物列表,并返回具有某种类型的事物的列表。 It would look like this:它看起来像这样:

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

Sadly, this is not possible.遗憾的是,这是不可能的。 The proposed signature for this extension method would have looked like:此扩展方法的建议签名如下所示:

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

... and the call to ThatAre<> fails because both type arguments need to be specified, even though TSource may be inferred from the usage. ... 并且对 ThatAre<> 的调用失败,因为需要指定两个类型参数,即使可以从用法中推断出 TSource。

Following the advice in other answers, I created two functions: one which captures the source, and another which allows callers to express the result:按照其他答案中的建议,我创建了两个函数:一个捕获源,另一个允许调用者表达结果:

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;
        }
    }
}

This results in the following calling code:这导致以下调用代码:

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

... which isn't bad. ......这还不错。

Notice that because of the generic type constraints, the following code:请注意,由于泛型类型约束,以下代码:

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

will fail to compile at the Are() step, since Trucks are not Fruits.将无法在 Are() 步骤编译,因为卡车不是水果。 This beats the provided .OfType<> function:这击败了提供的 .OfType<> 函数:

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

This compiles, but always yields zero results and indeed doesn't make any sense to try.这可以编译,但总是产生零结果并且确实没有任何意义尝试。 It's much nicer to let the compiler help you spot these things.让编译器帮助您发现这些东西要好得多。

If you have only two specific types of registration (which seems to be the case in your question), you could simply implement two extension methods:如果您只有两种特定类型的注册(您的问题似乎就是这种情况),您可以简单地实现两种扩展方法:

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

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

Then you wouldn't need to specify the type argument, so the type inference would work in the example you mentioned.那么您就不需要指定类型参数,因此类型推断将在您提到的示例中起作用。 Note that you can implement both of the extension methods just by delegation to a single generic extension method with two type parameters (the one in your question).请注意,您可以仅通过委托给具有两个类型参数(您问题中的那个)的单个通用扩展方法来实现这两种扩展方法。


In general, C# doesn't support anything like o.Foo<int, ?>(..) to infer only the second type parameter (it would be nice feature - F# has it and it's quite useful :-)).一般来说,C# 不支持像o.Foo<int, ?>(..)来推断第二个类型参数(这将是一个很好的功能 - F# 有它并且它非常有用:-))。 You could probably implement a workaround that would allow you to write this (basically, by separating the call into two method calls, to get two places where the type inferrence can be applied):您可能可以实现一种解决方法,允许您编写此代码(基本上,通过将调用分为两个方法调用,以获得可以应用类型推断的两个位置):

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

Here is a pseudo-code to demonstrate the structure:这是一个用于演示结构的伪代码:

// 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);
}

Why don't you specify zero type parameters?为什么不指定零类型参数? Both can be inferred in your sample.两者都可以在您的样本中推断出来。 If this is not an acceptable solution for you, I'm frequently encountering this problem too and there's no easy way to solve the problem "infer only one type parameter".如果这对您来说不是一个可接受的解决方案,我也经常遇到这个问题,并且没有简单的方法来解决“仅推断一个类型参数”的问题。 So I'll go with the duplicate methods.所以我将使用重复的方法。

What about the following:以下情况如何:

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

Then cast the parameter so the inference engine gets the right type:然后转换参数以便推理引擎获得正确的类型:

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

I think you need to split the two type parameters between two different expressions;我认为您需要在两个不同的表达式之间拆分两个类型参数; make the explicit one be part of the type of a parameter to the extension method, so inference can then pick it up.使显式成为扩展方法参数类型的一部分,以便推断可以将其提取。

Suppose you declared a wrapper class:假设您声明了一个包装类:

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

    public TValue Value { get; private set; }
}

Then your extension method as:然后你的扩展方法为:

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;
    }
}

Plus a simpler overload (the above could in fact call this one):加上一个更简单的重载(上面实际上可以称之为这个重载):

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

Now in the simple case where you are happy to infer the parameter value type:现在在您很高兴推断参数值类型的简单情况下:

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

But in the case where you need to explicitly state the type, you can do so:但是在需要明确说明类型的情况下,您可以这样做:

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

Looks ugly, but hopefully rarer than the simple fully-inferred kind.看起来很丑,但希望比简单的完全推断类型更罕见。

Note that you could just have the no-wrapper overload and write:请注意,您可以只使用无包装器重载并写入:

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

But that of course has the disadvantage of failing at runtime if you get something wrong.但是,如果出现错误,这当然有在运行时失败的缺点。 Unfortunately away from my C# compiler right now, so apologies if this is way off.不幸的是,我现在远离我的 C# 编译器,所以很抱歉,如果这已经过去了。

I would used the solution:我会使用解决方案:

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; }
    }
}

See:看:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM