6

オーバーロード メソッドがあります。最初の実装は常に単一のオブジェクトを返し、2 番目の実装は常に列挙を返します。

メソッドをジェネリックにしてオーバーロードし、ジェネリック型が列挙可能な場合にコンパイラが非列挙メソッドにバインドしようとするのを制限したい...

class Cache
{
    T GetOrAdd<T> (string cachekey, Func<T> fnGetItem)
        where T : {is not IEnumerable}
    {
    }

    T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem)
    {
    }
}

一緒に使用するには...

{
    // The compile should choose the 1st overload
    var customer = Cache.GetOrAdd("FirstCustomer", () => context.Customers.First());

    // The compile should choose the 2nd overload
    var customers = Cache.GetOrAdd("AllCustomers", () => context.Customers.ToArray());
}

これは、私がここで侵害している単純な悪いコードの匂いですか、それとも上記のメソッドのあいまいさを解消して、コンパイラが常に呼び出しコードを正しく取得できるようにすることは可能ですか?

「メソッドの名前を変更する」以外の答えを出すことができる人に投票してください。

4

4 に答える 4

8

メソッドの 1 つの名前を変更します。List<T>Add メソッドと AddRange メソッドがあることに気付くでしょう。そのパターンに従います。アイテムに対して何かを行うことと、一連のアイテムに対して何かを行うことは、論理的に異なるタスクであるため、メソッドに異なる名前を付けます。

于 2010-10-08T14:14:50.020 に答える
5

これは、C# コンパイラがオーバーロード解決を実行する方法と、バインドするメソッドを決定する方法のため、サポートが難しいユース ケースです。

最初の問題は、制約がメソッドのシグネチャの一部ではなく、オーバーロードの解決とは見なされないことです。

克服しなければならない 2 番目の問題は、コンパイラが利用可能なシグネチャから最適な一致を選択することです。これは、ジェネリックを扱う場合、一般に、特に次のようなパラメータがある場合にSomeMethod<T>(T)、より適切な一致と見なされることを意味します。または。SomeMethod<T>( IEnumerable<T> )T[]List<T>

しかし、より基本的には、単一の値に対する操作と値のコレクションに対する操作が本当に同じ操作かどうかを考慮する必要があります。論理的に異なる場合は、わかりやすくするために別の名前を使用することをお勧めします。おそらく、単一のオブジェクトとオブジェクトのコレクションとの間のセマンティックの違いは意味がないと主張できるいくつかのユースケースがあります...しかし、その場合、なぜ2つの異なるメソッドを実装する必要があるのでしょうか? メソッドのオーバーロードが違いを表現する最良の方法であるかどうかは不明です。混乱を招きやすい例を見てみましょう。

Cache.GetOrAdd("abc", () => context.Customers.Frobble() );

まず、上記の例では、戻りパラメーターを無視することを選択していることに注意してください。Frobble()次に、Customersコレクションに対して何らかのメソッドを呼び出していることに注意してください。GetOrAdd()では、どのオーバーロードが呼び出されるか教えていただけますか? 明らかに、Frobble()返す型を知らなければ、それは不可能です。個人的には、セマンティクスが構文から容易に推測できないコードは、可能な限り避けるべきだと考えています。より適切な名前を選択すると、この問題は軽減されます。

Cache.Add( "abc", () => context.Customers.Frobble() );
Cache.AddRange( "xyz", () => context.Customers.Frobble() );

最終的に、例のメソッドを明確にするためのオプションは 3 つしかありません。

  1. いずれかのメソッドの名前を変更します。
  2. IEnumerable<T>2 番目のオーバーロードを呼び出す場所にキャストします。
  3. コンパイラが区別できるように、メソッドの 1 つのシグネチャを変更します。

オプション 1 は自明であるため、これ以上は説明しません。

オプション 2 も理解しやすいです。

var customers = Cache.GetOrAdd("All", 
     () => (IEnumerable<Customer>)context.Customers.ToArray());

オプション 3 はより複雑です。それを達成する方法を見てみましょう。

アプローチでは、デリゲートの署名を変更Func<>します。たとえば、次のようになります。

 T GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem)
T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem)

// now we can do:
var customer = Cache.GetOrAdd("First", _ => context.Customers.First());
var customers = Cache.GetOrAdd("All", () => context.Customers.ToArray());

個人的には、このオプションはひどく醜く、直感的ではなく、混乱を招くと思います。未使用のパラメーターを導入するのはひどいです...しかし、残念ながらうまくいきます。

署名を変更するもう 1 つの方法 (それほどひどい方法ではありません) は、戻り値をoutパラメーターにすることです。

void GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem, out T);
void GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem, out T[])

// now we can write:
Customer customer;
Cache.GetOrAdd("First", _ => context.Customers.First(), out customer);

Customer[] customers;
var customers = Cache.GetOrAdd("All", 
                               () => context.Customers.ToArray(), out customers);

しかし、これは本当に良いですか?これらのメソッドを他のメソッド呼び出しのパラメーターとして使用できなくなります。また、コードが不明確になり、理解しにくくなります、IMO。

提示する最後の代替手段は、戻り値の型を識別する別の汎用パラメーターをメソッドに追加することです。

T GetOrAdd<T> (string cachekey, Func<T> fnGetItem);
R[] GetOrAdd<T,R> (string cachekey, Func<IEnumerable<T>> fnGetItem);

// now we can do:
var customer = Cache.GetOrAdd("First", _ => context.Customers.First());
var customers = Cache.GetOrAdd<Customer,Customer>("All", () => context.Customers.ToArray());

したがって、ヒントを使用して、コンパイラがオーバーロードを選択するのを助けることができます...確かに。しかし、そこにたどり着くために開発者としてしなければならない余分な作業をすべて見てください (導入された醜さと間違いの可能性は言うまでもありません)。それは本当に努力する価値がありますか?特に、私たちを助けるための簡単で信頼できる手法 (メソッドに別の名前を付ける) が既に存在する場合はどうでしょうか?

于 2010-10-08T14:31:28.193 に答える
2

IEnumerable<T>一般的な制約を介して不可能なことを試みるのではなく、1 つのメソッドのみを使用して動的にケースを検出します。保存/取得するオブジェクトが列挙可能なものであるかどうかに応じて、2 つの異なるキャッシュ メソッドを処理する必要があるのは「コードの匂い」です。また、実装しているからといって、IEnumerable<T>必ずしもコレクションであるとは限りません。

于 2010-10-08T14:15:27.273 に答える
1

制約は除外をサポートしていません。これは最初はイライラするように見えるかもしれませんが、一貫性があり、理にかなっています(たとえば、インターフェイスが実装で実行できないことを指示しないことを考慮してください)。

そうは言っても、IEnumerableオーバーロードの制約を試してみることができます...多分、メソッドを変更して、「 」<X, T>のような制約を持つ2つの一般的な型を設定します。where X : IEnumerable<T>

次のコードサンプルをETAします。

  void T[] GetOrAdd<X,T> (string cachekey, Func<X> fnGetItem) 
            where X : IEnumerable<T>
    { 
    }
于 2010-10-08T14:00:16.747 に答える