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


19

私はよくオンラインで質問を探しますが、多くのソリューションには辞書が含まれています。しかし、それらを実装しようとするたびに、コードにこの恐ろしい悪臭が入ります。たとえば、値を使用するたびに:

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

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

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

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

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

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


7
他の方法もありますが、通常は、値を取得する前に最初にContainsKeyを使用します
ロビーディー

2
正直なところ、いくつかの例外はありますが、辞書のキーが事前にわかっている場合、おそらくもっと良い方法があります。わからない場合は、通常、dictキーをまったくハードコーディングしないでください。辞書は、フィールドが少なくともアプリケーションに対してほとんど直交している半構造化オブジェクトまたはデータを操作するのに役立ちます。IMOでは、これらのフィールドが関連するビジネス/ドメインの概念に近づくほど、それらの概念を扱うのに役立つ辞書が少なくなります。
svidgen

6
@RobbieDee:ただし、競合状態が発生するため、その場合は注意が必要です。ContainsKeyを呼び出してから値を取得するまでにキーを削除することができます。
whatsisname

12
@whatsisname:これらの条件下では、ConcurrentDictionaryの方が適しています。System.Collections.Generic名前空間のコレクションはスレッドセーフではありません
ロバートハーベイ

4
私が誰かがを使用しているのを見る時間のかなりの50%は、Dictionary彼らが本当に欲しかったのは新しいクラスでした。これらの50%(のみ)にとっては、デザインの匂いです。
BlueRaja-Danny Pflughoeft

回答:


23

辞書(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のユーザーはより迅速にコードを学習できます。


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

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

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

問題のあるドメインを考えて、仕事に最適なツールを選択してください。存在しない場合は、作成します。


4
「その後、タプルのリストのみが必要な場合があります。または、マッピングの両側で最初の一致を簡単に見つけることができる構造体のリストもあります。」-小さいセットに最適化がある場合は、ユーザーコードにゴム印を付けないで、ライブラリで最適化する必要があります。
Blrfl

タプルのリストまたは辞書を選択した場合、それが実装の詳細です。問題の領域を理解し、仕事に適切なツールを使用するという問題です。明らかに、リストが存在し、辞書が存在します。一般的な場合、辞書は正しいですが、1つまたは2つのアプリケーションでは、リストを使用する必要があります。
ベリン・ロリッチ7

3
それは同じですが、動作が同じ場合、その決定はライブラリ内にある必要があります。エントリの数が少ないことをアプリオリに伝えられ場合アルゴリズムを切り替える他の言語のコンテナ実装を実行しました。私はあなたの答えに同意しますが、これがわかったら、ライブラリ、おそらくSmallBidirectionalMapに入れるべきです。
Blrfl

5
「良くも悪くも、C#ライブラリの設計者はこの動作に対処するイディオムを思いつきました。」–私はあなたが頭に釘を打ったと思う:彼らはイディオムで「上がった」、オプションのリターンタイプである既存の広く使われているものを使う代わりに。
イェルクWミッターク

3
@JörgWMittagのようなものについて話している場合Option<T>、パターンマッチングがないため、C#2.0でメソッドを使用するのがはるかに難しくなると思います。
svick

21

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

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

したがって、比較的最近の言語の追加を使用して、コンパクトなフォームを実現できます。


1
v7構文の良い呼び出し、var +1の使用を許可しながら余分な定義行を切り取らなければならない素敵な小さなオプション
BrianH

2
"key"存在しない場合、x初期化されることに注意してくださいdefault(TValue)
Peter Duniho

一般的な拡張機能としてもいいかもしれません。次のように呼び出されます"key".DoSomethingWithThis()
Ross Presser

getOrElse("key", defaultValue) Nullオブジェクトを使用するデザインは今でも私のお気に入りのパターンです。そのように動作し、TryGetValuetrueまたはfalseを返すかどうかは気にしません。
candied_orange

1
この答えは、コンパクトであるためにコンパクトなコードを書くのは悪いという免責事項に値すると思います。コードが読みにくくなる可能性があります。さらに、私が正しく思い出すと、TryGetValueアトミック/スレッドセーフではないため、存在のチェックと値の取得と操作のための別のチェックを簡単に実行できます
Marie

12

TryGetスタイルの関数をoutパラメーターとともに使用するのは慣用的な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が存在する全体の理由は、ContainsKeyとItem [TKey]を使用するより便利な方法として機能する一方で、辞書を2回検索する必要を回避するためです。選択。

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

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

CA1021:出力パラメーターを避ける

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

そこが、出力パラメータがアンチパターンのようなものだと聞いた場所だと思います。すべてのルールと同様に、「なぜ」を理解するために詳しく読む必要があります。この場合、Tryパターンがルールに違反していないことについての明示的な言及もあります

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


ガイダンスの全リストは、もっと多くの人に読んでほしいものです。フォローする人のために、ここにそのルートがあります。docs.microsoft.com/en-us/visualstudio/code-quality/...
ピーターwOneの

10

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

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

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

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

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


私は2番目のオプションが好きです。たぶん、拡張メソッドを1つまたは2つ書く必要があります:)
Adam B

2
プラス側のc#拡張メソッドは、これらを自分で実装できることを意味します
jk。

5

これは、基本的に次のことを行う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?();

これでTryGetValue、戻り値の新しいバージョンができましたint?。次に、同様の拡張のトリックを実行できます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キーワードを使用すると、関数のパラメーターが変化するため、アンチパターンであると聞きました。

言葉遣いはやや少なくなりますが、可能な場合は突然変異を避けることをお勧めします。

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

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

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

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

もちろんです。

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

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


ありがとう。アクションメソッドが実際に行っていることについてもう少し説明できると思いますか?私はアクションが初めてです。
アダムB

1
@AdamBアクションは、voidメソッドを呼び出す機能を表すオブジェクトです。ご覧のとおり、呼び出し側では、パラメーターリストがアクションのジェネリック型引数と一致するvoidメソッドの名前を渡します。呼び出し側では、他のメソッドと同様にアクションが呼び出されます。
エリックリッパー

1
@AdamB:Action<T>に非常に似ていると考えることができますが、interface IAction<T> { void Invoke(T t); }そのインターフェイスを「実装する」もの、およびどのInvokeように呼び出すことができるかについて非常に緩やかなルールがあります。詳細を知りたい場合は、C#の「デリゲート」について学習してから、ラムダ式について学習してください。
エリックリッパート

わかった。そのため、このアクションでは、DoIt()のパラメーターとしてメソッドを設定できます。そしてそのメソッドを呼び出します。
アダムB

@AdamB:そのとおりです。これは、「高階関数を使用した関数型プログラミング」の例です。ほとんどの関数は引数としてデータを取ります。高階関数は引数として関数を取り、それらの関数で何かをします。C#をさらに学習すると、LINQが完全に高階関数で実装されていることがわかります。
エリックリッパー

4

TryGetValue()「キー」がディクショナリ内のキーとして存在するかどうかわからない場合にのみ、コンストラクトが必要DoSomethingWith(dict["key"])です。そうでなければ、完全に有効です。

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


6
「「ダーティの少ない」アプローチは、ContainsKey()を代わりにチェックとして使用することです。」同意しません。現状でTryGetValueは準最適ですが、少なくとも空のケースを処理することを忘れることは困難です。理想的には、これがOptionalを返すことを望みます。
アレクサンダー-モニカの復活

1
ContainsKeyマルチスレッドアプリケーションのアプローチには潜在的な問題があります。これは、チェックから使用までの時間(TOCTOU)の脆弱性です。他のスレッドがとへの呼び出しの間にキーを削除するContainsKeyGetValueどうなりますか?
デビッドハメン

2
@JADオプションの構成。あなたは持つことができますOptional<Optional<T>>
復活モニカ-アレクサンダー

2
@DavidHammenを明確にするためには、必要があるだろうTryGetValueConcurrentDictionary。通常の辞書は同期されません
Alexander-Reinstate Monica

2
@JADいいえ、(Javaのオプションのタイプが打撃を与えます。始めないでください)。私はもっ​​と抽象的な話をしています
アレクサンダー-モニカを復活させる

2

他の回答には大きなポイントが含まれているので、ここではそれらを再度説明しませんが、代わりにこの部分に焦点を当てます。

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

実際には、次のように実装されているため、辞書を反復処理するのは非常に簡単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(これは、検索速度のために挿入速度を犠牲にしますが、固定の事前設定で見つめている場合に輝いている、紛らわしい名前の辞書です)ソート済みセット)。をに置き換えるDictionaryと、SortedDictionary実行速度が桁違いに速くなる場合がありました(ただし、逆の場合もあります)。


1

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.