簡體   English   中英

C# 中可以進行部分泛型類型推斷嗎?

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

我正在為我的 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<T> )能夠在ConcreteTypeRegistrationDelegateRegistration上調用,並且在這兩種情況下,返回類型都應該與調用擴展的類型相匹配。

問題如下:

我想寫:

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

而且Parameter<T>返回一個與它被調用的類型相同的對象,這意味着:

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

我想創建一個擴展方法,它可以枚舉事物列表,並返回具有某種類型的事物的列表。 它看起來像這樣:

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

這可以編譯,但總是產生零結果並且確實沒有任何意義嘗試。 讓編譯器幫助您發現這些東西要好得多。

如果您只有兩種特定類型的注冊(您的問題似乎就是這種情況),您可以簡單地實現兩種擴展方法:

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<int, ?>(..)來推斷第二個類型參數(這將是一個很好的功能 - 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);
}

為什么不指定零類型參數? 兩者都可以在您的樣本中推斷出來。 如果這對您來說不是一個可接受的解決方案,我也經常遇到這個問題,並且沒有簡單的方法來解決“僅推斷一個類型參數”的問題。 所以我將使用重復的方法。

以下情況如何:

使用您提供的定義: 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(...)))));

我認為您需要在兩個不同的表達式之間拆分兩個類型參數; 使顯式成為擴展方法參數類型的一部分,以便推斷可以將其提取。

假設您聲明了一個包裝類:

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# 編譯器,所以很抱歉,如果這已經過去了。

我會使用解決方案:

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

看:

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM