25

IoCクラスライブラリの流暢なインターフェイスの書き直しに取り組んでいます。基本クラスを通じていくつかの共通機能を共有するためにコードをリファクタリングしたときに、問題が発生しました。

:これは私がやりたいことであり、私しなければならないことではありません。別の構文でやらなければならない場合はそうしますが、コードを希望どおりにコンパイルする方法について誰かが考えている場合は、大歓迎です。

特定の基本クラスでいくつかの拡張メソッドを使用できるようにしたいのですが、これらのメソッドは、メソッドの引数に関連する1つのジェネリック型を持つジェネリックである必要がありますが、メソッドは、特定の子孫に関連する特定の型も返す必要があります。 'が呼び出されます。

上記の説明よりもコード例の方が優れています。

動作しないものの簡単で完全な例を次に示します。

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

コンパイラをだましてこの飛躍をさせる方法はありますか?

メソッドに2つのジェネリック型引数を追加すると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

これも同様に悪いことです。

クラスを簡単に再構築したり、メソッドを階層に導入することで非拡張メソッドにすることもできますが、私の質問は、2つの子孫のメソッドを複製する必要がなく、何らかの方法で1回だけ宣言できるかどうかです。 、基本クラスの場合。

言い換えれば。上記の最初のコード例のクラスを変更して、問題のメソッドを複製せずにMainメソッドの構文を保持できるようにする方法はありますか?

コードは、C#3.0と4.0の両方と互換性がある必要があります。


編集:両方のジェネリック型引数を推論に残したくない理由は、一部のサービスでは、1つの型のコンストラクターパラメーターにパラメーター値を指定し、子孫である値を渡すためです。今のところ、指定された引数値と呼び出す正しいコンストラクターの照合は、引数の名前とタイプの両方を使用して行われます。

例を挙げましょう:

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

4

6 に答える 6

18

モノのリストを列挙して、特定の型のもののリストを返すことができる拡張メソッドを作成したかったのです。次のようになります。

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 が使用法から推測される場合でも、両方の型引数を指定する必要があるためです。

他の回答のアドバイスに従って、ソースをキャプチャする関数と、呼び出し元が結果を表現できるようにする関数の 2 つの関数を作成しました。

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

これはコンパイルされますが、常に結果がゼロになり、実際に試しても意味がありません。これらのことを見つけるのをコンパイラーに手伝ってもらう方がずっといいです。

于 2010-11-05T14:30:37.290 に答える
15

特定のタイプの登録が2つしかない場合(質問の場合と思われます)、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); 

次に、型引数を指定する必要がないため、言及した例で型推論が機能します。2 つの型パラメーター (質問の 1 つ) を持つ単一の汎用拡張メソッドに委譲するだけで、両方の拡張メソッドを実装できることに注意してください。


一般に、C# は 2 番目の型パラメーターのみを推論するようなものをサポートしていませんo.Foo<int, ?>(..)(それは素晴らしい機能です - F# にはそれがあり、非常に便利です :-))。おそらく、これを記述できる回避策を実装できます (基本的には、呼び出しを 2 つのメソッド呼び出しに分けて、型推論を適用できる 2 つの場所を取得します)。

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);
}
于 2010-05-23T22:37:43.757 に答える
2

ゼロ型パラメータを指定しないのはなぜですか? どちらもサンプルで推測できます。これが受け入れられる解決策ではない場合、私もこの問題に頻繁に遭遇しており、「型パラメーターを 1 つだけ推論する」という問題を解決する簡単な方法はありません。したがって、重複する方法を使用します。

于 2010-05-23T22:21:45.743 に答える
1

次はどうでしょうか。

指定した定義を使用します。 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(...)))));
于 2010-05-23T22:47:13.313 に答える
0

2 つの型パラメーターを 2 つの異なる式に分割する必要があると思います。明示的なものを拡張メソッドへのパラメーターの型の一部にすることで、推論がそれを取得できるようにします。

ラッパー クラスを宣言したとします。

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# コンパイラから離れているため、これがかなりずれている場合はお詫び申し上げます。

于 2010-05-23T22:50:58.047 に答える
0

私は解決策を使用します:

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

見る:

于 2021-09-06T06:59:31.810 に答える