web-dev-qa-db-ja.com

C#の辞書キーとしてのタプルと文字列

ConcurrentDictionaryを使用して実装するキャッシュがあり、保持する必要があるデータは5つのパラメーターに依存します。したがって、キャッシュから取得する方法は次のとおりです(簡単にするためにここでは3つのパラメーターのみを示し、明確にするためにCarDataを表すようにデータ型を変更しました)

public CarData GetCarData(string carModel, string engineType, int year);

ConcurrentDictionaryでどのタイプのキーを使用するのが良いのか、このようにできます。

var carCache = new ConcurrentDictionary<string, CarData>();
// check for car key
bool exists = carCache.ContainsKey(string.Format("{0}_{1}_{2}", carModel, engineType, year);

またはこのように:

var carCache = new ConcurrentDictionary<Tuple<string, string, int>, CarData>();
// check for car key
bool exists = carCache.ContainsKey(new Tuple(carModel, engineType, year));

私はこれらのパラメーターを他の場所で一緒に使用しないので、それらを一緒に保つためだけにクラスを作成する正当な理由はありません。

パフォーマンスと保守性の観点から、どちらのアプローチが優れているかを知りたい。

27
Shahar

GetHashCodeとEqualsをオーバーライドするクラスを作成できます(ここで使用されているだけでかまいません)。

TheDmi(およびその他)の改善に感謝します...

public class CarKey : IEquatable<CarKey>
{
    public CarKey(string carModel, string engineType, int year)
    {
        CarModel = carModel;
        EngineType= engineType;
        Year= year;
    }

    public string CarModel {get;}
    public string EngineType {get;}
    public int Year {get;}

    public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = (int) 2166136261;

            hash = (hash * 16777619) ^ CarModel?.GetHashCode() ?? 0;
            hash = (hash * 16777619) ^ EngineType?.GetHashCode() ?? 0;
            hash = (hash * 16777619) ^ Year.GetHashCode();
            return hash;
        }
    }

    public override bool Equals(object other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        if (other.GetType() != GetType()) return false;
        return Equals(other as CarKey);
    }

    public bool Equals(CarKey other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(CarModel,obj.CarModel) && string.Equals(EngineType, obj.EngineType) && Year == obj.Year;
    }
}

これらをオーバーライドしない場合、ContainsKeyは同等の参照を行います。

注:Tupleクラスには、基本的に上記と同じ独自の等価関数があります。特注のクラスを使用すると、それが意図されていることが明確になり、保守性が向上します。また、プロパティに明確な名前を付けることができるという利点もあります

注2:オブジェクトが辞書に追加された後にハッシュコードが変わる潜在的なバグを避けるために辞書キーが必要であるため、クラスは不変です こちらを参照

ここから取得したGetHashCode

14
Tim Rutter

パフォーマンスと保守性の観点から、どちらのアプローチが優れているかを知りたい。

いつものように、あなたはそれを理解するためのツールを持っています。可能な解決策の両方をコーディングして、それらを作成しますrace。勝者が勝者です。この特定の質問に答えるためにここに誰かがいる必要はありません。

メンテナンスについては、自動ドキュメント化とスケーラビリティの向上を実現するソリューションが勝者になるはずです。この場合、コードは非常に簡単なので、自動ドキュメント化はそれほど問題ではありません。スケーラビリティの観点から見ると、私見では、最善の解決策はTuple<T1, T2, ...>を使用することです。

  • 維持する必要のない無料の平等セマンティクスを取得します。
  • 衝突は不可能です。文字列連結ソリューションを選択した場合は正しくありません。

    var param1 = "Hey_I'm a weird string";
    var param2 = "!"
    var param3 = 1;
    key = "Hey_I'm a weird string_!_1";
    
    var param1 = "Hey";
    var param2 = "I'm a weird string_!"
    var param3 = 1;
    key = "Hey_I'm a weird string_!_1";
    

    ええ、かなりフェッチされていますが、理論的には完全に可能であり、あなたの質問は将来の未知の出来事に関するものです。

  • そして最後に、コンパイラーhelpsが役立ちます。たとえば、明日、param4をキーに追加する必要がある場合、Tuple<T1, T2, T3, T4>はキーを強く入力します。一方、文字列連結アルゴリズムは、param4を使用せずにキーを生成することで非常に満足し、ソフトウェアが期待どおりに機能しないため、クライアントが電話をかけるまで何が起こるかわかりません。

21
InBetween

パフォーマンスが本当に重要な場合、答えはどちらのオプションも使用すべきではないということです。両方のアクセスでオブジェクトが不必要に割り当てられるためです。

代わりに、カスタムのstruct、または System.ValueTupleパッケージValueTupleを使用する必要があります。

var myCache = new ConcurrentDictionary<ValueTuple<string, string, int>, CachedData>();
bool exists = myCache.ContainsKey(ValueTuple.Create(param1, param2, param3));

C#7.0は、このコードを記述しやすくするために構文シュガーも備えています(ただし、C#7.0がシュガーなしでValueTupleの使用を開始するのを待つ必要はありません)。

var myCache = new ConcurrentDictionary<(string, string, int), CachedData>();
bool exists = myCache.ContainsKey((param1, param2, param3));
10
svick

カスタムキークラスを実装し、そのようなユースケースに適していることを確認します。つまり、IEquatableを実装し、クラスを不変にします

_public class CacheKey : IEquatable<CacheKey>
{
    public CacheKey(string param1, string param2, int param3)
    {
        Param1 = param1;
        Param2 = param2;
        Param3 = param3;
    }

    public string Param1 { get; }

    public string Param2 { get; }

    public int Param3 { get; }

    public bool Equals(CacheKey other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((CacheKey)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = Param1?.GetHashCode() ?? 0;
            hashCode = (hashCode * 397) ^ (Param2?.GetHashCode() ?? 0);
            hashCode = (hashCode * 397) ^ Param3;
            return hashCode;
        }
    }
}
_

これは、Resharperが生成するGetHashCode()実装です。これは、優れた汎用実装です。必要に応じて適応します。


または、EqualsおよびGetHashCode実装を自動的に生成する Eq (私はそのライブラリの作成者です)のようなものを使用します。これにより、これらのメソッドにCacheKeyクラスのすべてのメンバーが常に含まれるようになるため、コードのメンテナンスがはるかに容易になります。そのような実装は、次のようになります。

_public class CacheKey : MemberwiseEquatable<CacheKey>
{
    public CacheKey(string param1, string param2, int param3)
    {
        Param1 = param1;
        Param2 = param2;
        Param3 = param3;
    }

    public string Param1 { get; }

    public string Param2 { get; }

    public int Param3 { get; }
}
_

注:明らかに(意味のあるプロパティ名を使用する必要があります。そうでない場合、カスタムクラスを導入しても、Tuple

6
theDmi

他のコメントで説明されているTupleClass対 "id_id_id"アプローチを比較したいと思いました。私はこの単純なコードを使用しました:

public class Key : IEquatable<Key>
{
    public string Param1 { get; set; }
    public string Param2 { get; set; }
    public int Param3 { get; set; }

    public bool Equals(Key other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Key) obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = (Param1 != null ? Param1.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (Param2 != null ? Param2.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ Param3;
            return hashCode;
        }
    }
}

static class Program
{

    static void TestClass()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var classDictionary = new Dictionary<Key, string>();

        for (var i = 0; i < 10000000; i++)
        {
            classDictionary.Add(new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }, i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = classDictionary[new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void TestTuple()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var tupleDictionary = new Dictionary<Tuple<string, string, int>, string>();

        for (var i = 0; i < 10000000; i++)
        {
            tupleDictionary.Add(new Tuple<string, string, int>(i.ToString(), i.ToString(), i), i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = tupleDictionary[new Tuple<string, string, int>(i.ToString(), i.ToString(), i)];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void TestFlat()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var tupleDictionary = new Dictionary<string, string>();

        for (var i = 0; i < 10000000; i++)
        {
            tupleDictionary.Add($"{i}_{i}_{i}", i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = tupleDictionary[$"{i}_{i}_{i}"];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void Main()
    {
        TestClass();
        TestTuple();
        TestFlat();
    }
}

結果:

Releaseでデバッグせずに各メソッドを3回実行し、各実行で他のメソッドへの呼び出しをコメント化しました。 3回の実行の平均を取りましたが、とにかく大きな違いはありませんでした。

TestTuple:

initialization: 00:00:14.2512736
Retrieving: 00:00:08.1912167

TestClass:

initialization: 00:00:11.5091160
Retrieving: 00:00:05.5127963

TestFlat:

initialization: 00:00:16.3672901
Retrieving: 00:00:08.6512009

クラスアプローチがタプルアプローチとストリングアプローチの両方よりも高速であることに驚きました。私の意見では、Keyクラスにより多くの機能を追加できるという意味で、より読みやすく、より安全です(単なるキーではなく、何かを表します)。

4
Tomer

私見、私はそのような場合に中間構造を使用することを好みます(あなたの場合はTupleになります)。このようなアプローチは、パラメーターと最終ターゲットディクショナリーの間に追加のレイヤーを作成します。もちろん、それは目的に依存します。このような方法により、たとえば、パラメータの些細ではない遷移を作成できます(たとえば、コンテナがデータを「歪める」可能性があります)。

3
LmTinyToon

Tomerのテストケースを実行し、ValueTuplesをテストケースとして追加しました(新しいc#値型)。彼らのパフォーマンスに感銘を受けました。

TestClass
initialization: 00:00:11.8787245
Retrieving: 00:00:06.3609475

TestTuple
initialization: 00:00:14.6531189
Retrieving: 00:00:08.5906265

TestValueTuple
initialization: 00:00:10.8491263
Retrieving: 00:00:06.6928401

TestFlat
initialization: 00:00:16.6559780
Retrieving: 00:00:08.5257845

テストのコードは次のとおりです。

static void TestValueTuple(int n = 10000000)
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    var tupleDictionary = new Dictionary<(string, string, int), string>();

    for (var i = 0; i < n; i++)
    {
        tupleDictionary.Add((i.ToString(), i.ToString(), i), i.ToString());
    }
    stopwatch.Stop();
    Console.WriteLine($"initialization: {stopwatch.Elapsed}");

    stopwatch.Restart();

    for (var i = 0; i < n; i++)
    {
        var s = tupleDictionary[(i.ToString(), i.ToString(), i)];
    }

    stopwatch.Stop();
    Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
}
3
Grady Werner