web-dev-qa-db-ja.com

TryGetValueよりもC#辞書を使用するより良い方法はありますか?

オンラインで質問を検索することがよくありますが、多くの解決策には辞書が含まれています。しかし、それらを実装しようとするときはいつでも、コードにこの恐ろしい悪臭を放ちます。たとえば、値を使用するたびに:

_int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}
_

これは、基本的に次のことを行う4行のコードです。DoSomethingWith(dict["key"])

Outキーワードを使用すると、関数がパラメータを変更するため、アンチパターンであると聞きました。

また、キーと値を反転させる「逆」辞書が必要になることがよくあります。

同様に、辞書の項目を繰り返し処理して、キーや値をリストなどに変換して、より適切に処理したいと思うことがよくあります。

辞書を使用するより良い、よりエレガントな方法がほとんど常にあるように感じますが、私は途方に暮れています。

19
Adam B

辞書(C#など)は、単にキーに基づいて値を検索するコンテナーです。多くの言語では、それはマップとしてより正確に識別され、最も一般的な実装はHashMapです。

考慮すべき問題は、キーが存在しない場合にどうなるかです。一部の言語は、nullまたはnilまたは他の同等の値を返すことによって動作します。値が存在しないことを通知するのではなく、値をサイレントにデフォルト設定します。

良くも悪くも、C#ライブラリデザイナーはこの動作に対処するためのイディオムを考え出しました。彼らは、存在しない値を検索するためのデフォルトの動作は例外をスローすることであると推論しました。例外を避けたい場合は、Tryバリアントを使用できます。これは、文字列を整数または日付/時刻オブジェクトに解析するために使用するアプローチと同じです。基本的に、影響は次のとおりです。

_T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}
_

そして、それは辞書に引き継がれ、そのインデクサーはGet(index)に委任します。

_var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}
_

これは単に言語の設計方法です。


out変数は推奨されませんか?

C#は最初の言語ではなく、特定の状況で目的を持っています。高度な並行システムを構築しようとしている場合、並行性の境界でout変数を使用することはできません。

多くの点で、言語およびコアライブラリプロバイダーによって支持されているイディオムがある場合は、それらのイディオムをAPIに採用しようとします。これにより、その言語でのAPIの一貫性と使いやすさが向上します。したがって、Rubyで記述されたメソッドは、C#、C、またはPythonで記述されたメソッドのようには見えません。それぞれにコードを構築する好ましい方法があり、それを使用してユーザーはあなたのAPIのより速くそれを学びます。


マップは一般にアンチパターンですか?

それらには目的がありますが、多くの場合、それらはあなたが持っている目的に対して間違った解決策である可能性があります。特に、双方向のマッピングが必要な場合。多くのコンテナとデータを整理する方法があります。使用できるアプローチはたくさんありますが、そのコンテナを選択する前に少し考える必要がある場合があります。

双方向マッピング値の非常に短いリストがある場合は、タプルのリストのみが必要になることがあります。または、構造体のリスト。マッピングの両側で最初に一致したものを簡単に見つけることができます。

問題のある領域について考え、その仕事に最適なツールを選択してください。ない場合は作成します。

23
Berin Loritsch

ハッシュテーブル/辞書の一般原則について、ここでいくつかの良い答えがあります。しかし、私はあなたのコード例に触れたいと思いました、

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

C#7(約2歳だと思います)の時点で、次のように簡略化できます。

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

そしてもちろん、1行に減らすこともできます。

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

キーが存在しない場合のデフォルト値がある場合、次のようになります。

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

そのため、最近追加された言語を使用することで、コンパクトなフォームを実現できます。

23
David Arno

これはコードの匂いでもアンチパターンでもありません。出力パラメーターを指定したTryGetスタイルの関数を使用するのは慣用的なC#であるためです。ただし、C#にはディクショナリを操作するための3つのオプションが用意されているので、状況に応じて正しいオプションを使用していることを確認してください。 outパラメータの使用に問題があるという噂はどこから来たのかわかっていると思いますので、最後に扱います。

C#辞書を操作するときに使用する機能:

  1. キーがディクショナリにあることが確実な場合は、Item [TKey]プロパティを使用します
  2. キーが一般的にディクショナリにあるべきであるが、そこにないことが悪い/まれ/問題がある場合は、Try ... Catchを使用してエラーが発生し、エラーを適切に処理できるようにする必要があります。
  3. キーがディクショナリにあるかどうかわからない場合は、outパラメータを指定してTryGetを使用します

これを正当化するには、「備考」の下にある ディクショナリTryGetValueの/-ドキュメントを参照するだけでよい

このメソッドは、ContainsKeyメソッドの機能とItem [TKey]プロパティの機能を組み合わせたものです。

...

コードが辞書にないキーに頻繁にアクセスする場合は、TryGetValueメソッドを使用します。このメソッドを使用すると、Item [TKey]プロパティによってスローされたKeyNotFoundExceptionをキャッチするよりも効率的です。

このメソッドは、O(1)演算に近づきます。

TryGetValueが存在する理由は、辞書を2回検索する必要がないので、ContainsKeyとItem [TKey]を使用するためのより便利な方法として機能するためです。選択。

実際には、この単純な格言のため、生のディクショナリを使用することはほとんどありません。必要な機能を提供する最も一般的なクラス/コンテナを選択してください。ディクショナリは、キーなどではなく値で検索するようには設計されていないため、それが必要な場合は、代替構造を使用する方が理にかなっています。去年の開発プロジェクトで辞書を一度使用したことがあると思います。それは、私がやろうとしていた仕事に適したツールであることがめったになかったからです。辞書は確かにC#ツールボックスのスイスアーミーナイフではありません。

outパラメータの何が問題になっていますか?

CA1021:出力パラメーターを回避

戻り値は一般的で頻繁に使用されますが、outパラメータとrefパラメータを正しく適用するには、中間的な設計とコーディングのスキルが必要です。一般ユーザー向けに設計するライブラリアーキテクトは、ユーザーがoutまたはrefパラメーターの操作を習得することを期待すべきではありません。

私はあなたがアウトパラメータがアンチパターンのようなものであったと聞いたところだと思います。すべてのルールと同様に、「なぜ」を理解するためによく読んでください。この場合、 試行パターンがルールに違反しない方法 についての明示的な言及もあります。

System.Int32.TryParseなどのTryパターンを実装するメソッドでは、この違反は発生しません。

12
BrianH

他の言語の多くの状況でコードを大幅にクリーンアップするC#辞書には、少なくとも2つのメソッドがありません。 1つ目はOptionを返すことです。これにより、Scalaで次のようなコードを記述できます。

dict.get("key").map(doSomethingWith)

2つ目は、キーが見つからない場合にユーザー指定のデフォルト値を返します。

doSomethingWith(dict.getOrElse("key", "key not found"))

Tryパターンのように、適切な場合に言語が提供するイディオムを使用するために言うべきことがありますが、それはonly言語が提供するものを使用する必要があるという意味ではありません。私たちはプログラマーです。特定の状況を理解しやすくするために、特に多くの繰り返しを排除する場合は、新しい抽象化を作成してもかまいません。逆ルックアップや値の反復など、頻繁に何かが必要な場合は、それを実現します。希望するインターフェースを作成します。

10
Karl Bielefeldt

これは、基本的に次のことを行う4行のコードです。DoSomethingWith(dict["key"])

これは洗練されていないことに同意します。この場合、値が構造体型である場合に使用したいメカニズムは次のとおりです。

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

これで、int?を返すTryGetValueの新しいバージョンができました。次に、T?を拡張するために同様のトリックを実行できます。

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

そして今それをまとめます:

dict.TryGetValue("key").DoIt(DoSomethingWith);

そして、私たちは単一の明確な声明にたどり着きました。

Outキーワードを使用すると、関数がパラメーターを変更するため、アンチパターンであると聞きました。

言葉遣いはやや控えめですが、可能な場合は突然変異を回避することをお勧めします。

キーと値を入れ替える「逆」辞書が必要になることがよくあります。

次に、双方向辞書を実装または取得します。彼らは書くのが簡単です、またはインターネット上で利用可能な実装がたくさんあります。ここにはたくさんの実装があります、例えば:

https://stackoverflow.com/questions/268321/bidirectional-1-to-1-dictionary-in-c-sharp

同様に、辞書の項目を繰り返し処理して、キーや値をリストなどに変換して、より適切に処理したいと思うことがよくあります。

もちろん、私たちは皆そうです。

辞書を使用するより良い、よりエレガントな方法がほとんど常にあるように感じますが、私は途方に暮れています。

「私がしたい正確な操作を実装したDictionary以外のクラスがあるとしましょう。そのクラスはどのように見えるでしょうか?」次に、その質問に答えたら、そのクラスを実装します。あなたはコンピュータープログラマーです。コンピュータプログラムを書いて問題を解決しましょう!

5
Eric Lippert

TryGetValue()構成は、 "key"が辞書内のキーとして存在するかどうかわからない場合にのみ必要です。それ以外の場合は、DoSomethingWith(dict["key"])は完全に有効です。

「ダーティーが少ない」アプローチは、代わりにContainsKey()をチェックとして使用することです。

4
Peregrine

他の回答には優れた点が含まれているので、ここでは再説明しませんが、代わりにこの部分に焦点を当てます。これは、今のところほとんど無視されているようです。

同様に、辞書の項目を繰り返し処理して、キーや値をリストなどに変換して、より適切に処理したいと思うことがよくあります。

実際、IEnumerableを実装しているため、辞書を反復するのは非常に簡単です。

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

あなたがLinqを好むなら、それもうまくいきます:

dict.Select(x=>x.Value).WhateverElseYouWant();

全体として、私は辞書がアンチパターンであるとは思いません-それらは特定の用途を持つ単なる特定のツールです。

また、さらに詳細については、 SortedDictionary (より予測可能なパフォーマンスのためにRBツリーを使用)および SortedList (これも紛らわしい名前の辞書で、挿入速度を犠牲にしてルックアップ速度を犠牲にしますが、固定され、事前にソートされたセットで凝視している場合は優れています。 DictionarySortedDictionaryに置き換えると、実行が1桁速くなる場合がありました(ただし、逆の場合も発生する可能性があります)。

2
Vilx-

辞書を使用するのが面倒だと感じる場合、それはあなたの問題にとって正しい選択ではないかもしれません。辞書は素晴らしいですが、1人のコメンターが気づいたように、クラスであるはずの何かのショートカットとしてよく使用されます。または、ディクショナリ自体がコアストレージメソッドとして正しいかもしれませんが、必要なサービスメソッドを提供するために、その周りにラッパークラスが必要です。

Dict [key]とTryGetについて多くのことが言われてきました。私がよく使用するのは、KeyValuePairを使用して辞書を反復処理することです。どうやらこれはあまり一般的に知られていない構成です。

辞書の主な利点は、アイテムの数が増えるにつれて、他のコレクションと比べて非常に高速になることです。キーが多く存在するかどうかを確認する必要がある場合は、辞書の使用が適切かどうかを自問してみてください。クライアントとして、あなたは通常何をそこに入れたか、したがって何を安全にクエリできるかを知っているべきです。

1
Martin Maat