Có cách nào tốt hơn để sử dụng từ điển C # hơn so với TryGetValue không?


19

Tôi thấy mình thường tìm kiếm các câu hỏi trực tuyến và nhiều giải pháp bao gồm từ điển. Tuy nhiên, bất cứ khi nào tôi cố gắng thực hiện chúng, tôi nhận được mã số khủng khiếp này trong mã của mình. Ví dụ: mỗi lần tôi muốn sử dụng một giá trị:

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

Đó là 4 dòng mã về cơ bản để làm như sau: DoSomethingWith(dict["key"])

Tôi đã nghe nói rằng sử dụng từ khóa out là một kiểu chống vì nó làm cho các hàm biến đổi các tham số của chúng.

Ngoài ra, tôi thấy mình thường cần một từ điển "đảo ngược", nơi tôi lật các phím và giá trị.

Tương tự, tôi thường muốn lặp qua các mục trong từ điển và thấy mình chuyển đổi các khóa hoặc giá trị thành danh sách, v.v. để làm điều này tốt hơn.

Tôi cảm thấy gần như luôn luôn có cách sử dụng từ điển tốt hơn, thanh lịch hơn, nhưng tôi cảm thấy hụt hẫng.


7
Các cách khác có thể tồn tại nhưng tôi thường sử dụng ContainsKey trước khi thử lấy giá trị.
Robbie Dee

2
Thành thực mà nói, với một vài ngoại lệ, nếu bạn biết những gì các phím từ điển của bạn đang ở phía trước của thời gian, có lẽ một cách tốt hơn. Nếu bạn không biết, các khóa dict thường không nên được mã hóa cứng. Từ điển rất hữu ích để làm việc với các đối tượng hoặc dữ liệu bán cấu trúc trong đó các trường ít nhất trực giao với ứng dụng. IMO, các lĩnh vực càng trở nên gần gũi hơn với các khái niệm kinh doanh / tên miền có liên quan, các từ điển ít hữu ích trở nên để làm việc với các khái niệm đó.
Svidgen

6
@RobbieDee: bạn phải cẩn thận khi làm điều đó, vì làm như vậy sẽ tạo ra một điều kiện cuộc đua. Có thể khóa có thể bị xóa giữa khi gọi ContainsKey và nhận giá trị.
whatsisname

12
@whatsisname: Trong những điều kiện đó, một từ đồng thời sẽ phù hợp hơn. Bộ sưu tập trong System.Collections.Generickhông gian tên không an toàn cho chuỗi .
Robert Harvey

4
50% thời gian tôi thấy ai đó sử dụng a Dictionary, điều họ thực sự muốn là một lớp mới. Đối với những người 50% (chỉ), đó là một mùi thiết kế.
BlueRaja - Daniel Pflughoeft

Câu trả lời:


23

Từ điển (C # hoặc cách khác) chỉ đơn giản là một thùng chứa nơi bạn tìm kiếm một giá trị dựa trên khóa. Trong nhiều ngôn ngữ, nó được xác định chính xác hơn là Bản đồ với cách triển khai phổ biến nhất là HashMap.

Vấn đề cần xem xét là những gì xảy ra khi một khóa không tồn tại. Một số ngôn ngữ hành xử bằng cách trả lại nullhoặc nilhoặc một số giá trị tương đương khác. Âm thầm mặc định một giá trị thay vì thông báo cho bạn rằng một giá trị không tồn tại.

Để tốt hơn hay tồi tệ hơn, các nhà thiết kế thư viện C # đã đưa ra một thành ngữ để đối phó với hành vi. Họ lập luận rằng hành vi mặc định để tìm kiếm một giá trị không tồn tại là ném ngoại lệ. Nếu bạn muốn tránh ngoại lệ, thì bạn có thể sử dụng Trybiến thể. Đó là cách tiếp cận tương tự mà họ sử dụng để phân tích chuỗi thành các số nguyên hoặc đối tượng ngày / giờ. Về cơ bản, tác động là như thế này:

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

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

Và điều đó đã được chuyển đến từ điển, mà người chỉ mục ủy nhiệm cho Get(index):

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

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

Đây chỉ đơn giản là cách ngôn ngữ được thiết kế.


Các outbiến có nên được khuyến khích?

C # không phải là ngôn ngữ đầu tiên có chúng và chúng có mục đích của chúng trong các tình huống cụ thể. Nếu bạn đang cố gắng xây dựng một hệ thống đồng thời cao, thì bạn không thể sử dụng outcác biến ở ranh giới đồng thời.

Theo nhiều cách, nếu có một thành ngữ được các nhà cung cấp thư viện cốt lõi và ngôn ngữ tán thành, tôi cố gắng áp dụng các thành ngữ đó trong API của mình. Điều đó làm cho API cảm thấy nhất quán hơn và ở nhà theo ngôn ngữ đó. Vì vậy, một phương thức được viết bằng Ruby sẽ không giống như một phương thức được viết bằng C #, C hoặc Python. Mỗi người trong số họ có một cách xây dựng mã ưa thích và làm việc với điều đó giúp người dùng API của bạn học nó nhanh hơn.


Bản đồ nói chung có phải là mẫu chống không?

Họ có mục đích của họ, nhưng nhiều lần họ có thể là giải pháp sai cho mục đích bạn có. Đặc biệt nếu bạn có một bản đồ hai chiều bạn cần. Có rất nhiều container và cách tổ chức dữ liệu. Có nhiều cách tiếp cận mà bạn có thể sử dụng, và đôi khi bạn cần suy nghĩ một chút trước khi chọn thùng chứa đó.

Nếu bạn có một danh sách rất ngắn các giá trị ánh xạ hai chiều, thì bạn có thể chỉ cần một danh sách các bộ dữ liệu. Hoặc một danh sách các cấu trúc, nơi bạn có thể dễ dàng tìm thấy trận đấu đầu tiên ở hai bên của ánh xạ.

Hãy nghĩ về miền vấn đề và chọn công cụ phù hợp nhất cho công việc. Nếu không có, thì hãy tạo nó.


4
"sau đó bạn có thể chỉ cần một danh sách các bộ dữ liệu. Hoặc một danh sách các cấu trúc, nơi bạn có thể dễ dàng tìm thấy trận đấu đầu tiên ở hai bên của ánh xạ." - Nếu có tối ưu hóa cho các bộ nhỏ, chúng nên được thực hiện trong thư viện, không được dán tem cao su vào mã người dùng.
Blrfl

Nếu bạn chọn một danh sách các bộ dữ liệu hoặc một từ điển, đó là một chi tiết thực hiện. Đó là một câu hỏi để hiểu miền vấn đề của bạn và sử dụng công cụ phù hợp cho công việc. Rõ ràng, một danh sách tồn tại và một từ điển tồn tại. Đối với trường hợp phổ biến, từ điển là chính xác, nhưng có thể đối với một hoặc hai ứng dụng bạn cần sử dụng danh sách.
Berin Loritsch

3
Đó là, nhưng nếu hành vi vẫn như vậy, quyết tâm đó nên có trong thư viện. Tôi đã chạy trên các triển khai container trong các ngôn ngữ khác sẽ chuyển đổi thuật toán nếu được nói với một tiên nghiệm rằng số lượng mục nhập sẽ nhỏ. Tôi đồng ý với câu trả lời của bạn, nhưng một khi điều này đã được tìm ra, nó sẽ ở trong một thư viện, có thể là Bản đồ nhỏ.
Blrfl

5
"Để tốt hơn hay tồi tệ hơn, các nhà thiết kế thư viện C # đã đưa ra một thành ngữ để đối phó với hành vi." - Tôi nghĩ rằng bạn đánh vào đầu đinh: họ "nghĩ ra" một thành ngữ, thay vì sử dụng một từ được sử dụng rộng rãi hiện có, đó là một kiểu trả về tùy chọn.
Jörg W Mittag

3
@ JörgWMittag Nếu bạn đang nói về một cái gì đó như thế Option<T>, thì tôi nghĩ rằng điều đó sẽ khiến phương thức này khó sử dụng hơn nhiều trong C # 2.0, vì nó không có khớp mẫu.
Svick

21

Một số câu trả lời tốt ở đây về các nguyên tắc chung của hashtables / từ điển. Nhưng tôi nghĩ tôi sẽ chạm vào ví dụ mã của bạn,

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

Kể từ C # 7 (mà tôi nghĩ là khoảng hai tuổi), có thể được đơn giản hóa thành:

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

Và tất nhiên nó có thể được giảm xuống một dòng:

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

Nếu bạn có một giá trị mặc định khi khóa không tồn tại, nó có thể trở thành:

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

Vì vậy, bạn có thể đạt được các hình thức nhỏ gọn bằng cách sử dụng bổ sung ngôn ngữ hợp lý gần đây.


1
Gọi tốt cú pháp v7, tùy chọn nhỏ xinh để cắt bỏ dòng định nghĩa bổ sung trong khi cho phép sử dụng var +1
BrianH

2
Cũng lưu ý rằng nếu "key"không có mặt, xsẽ được khởi tạo thànhdefault(TValue)
Peter Duniho

Cũng có thể tốt đẹp như một phần mở rộng chung, được gọi như"key".DoSomethingWithThis()
Ross Presser

Tôi rất thích các thiết kế sử dụng getOrElse("key", defaultValue) đối tượng Null vẫn là mẫu yêu thích của tôi. Làm việc theo cách đó và bạn không quan tâm nếu TryGetValuetrả về đúng hay sai.
candied_orange

1
Tôi cảm thấy câu trả lời này xứng đáng được từ chối rằng viết mã nhỏ gọn vì lợi ích của nó là nhỏ gọn. Nó có thể làm cho mã của bạn khó đọc hơn nhiều. Ngoài ra, nếu tôi nhớ chính xác, TryGetValuekhông phải là nguyên tử / luồng an toàn, do đó bạn có thể dễ dàng thực hiện một kiểm tra cho sự tồn tại và một kiểm tra khác để lấy và vận hành theo giá trị
Marie

12

Đây không phải là mùi mã cũng không phải là kiểu chống mẫu, vì sử dụng các hàm kiểu TryGet với tham số out là C # thành ngữ. Tuy nhiên, có 3 tùy chọn được cung cấp trong C # để hoạt động với Từ điển, vì vậy bạn nên chắc chắn rằng mình đang sử dụng đúng tùy chọn cho tình huống của mình. Tôi nghĩ rằng tôi biết tin đồn về việc có một vấn đề khi sử dụng tham số out xuất phát từ đâu, vì vậy tôi sẽ xử lý vấn đề đó ở cuối.

Những tính năng để sử dụng khi làm việc với Từ điển C #:

  1. Nếu bạn chắc chắn khóa sẽ nằm trong Từ điển, hãy sử dụng thuộc tính Mục [TKey]
  2. Nếu khóa thường nằm trong Từ điển, nhưng thật tệ / hiếm / có vấn đề là nó không có ở đó, bạn nên sử dụng Thử ... Bắt để một lỗi sẽ được đưa ra và sau đó bạn có thể cố gắng xử lý lỗi một cách duyên dáng
  3. Nếu bạn không chắc chắn khóa sẽ có trong Từ điển, hãy sử dụng TryGet với tham số out

Để biện minh cho điều này, người ta chỉ cần tham khảo tài liệu về Từ điển TryGetValue, trong phần "Nhận xét" :

Phương thức này kết hợp chức năng của phương thức ContainsKey và thuộc tính Item [TKey].

...

Sử dụng phương thức TryGetValue nếu mã của bạn thường xuyên cố gắng truy cập các khóa không có trong từ điển. Sử dụng phương pháp này hiệu quả hơn so với việc bắt KeyNotFoundException được ném bởi thuộc tính Item [TKey].

Phương pháp này tiếp cận một hoạt động O (1).

Toàn bộ lý do TryGetValue tồn tại là hoạt động như một cách thuận tiện hơn để sử dụng ContainsKey và Item [TKey], trong khi tránh phải tìm kiếm từ điển hai lần - vì vậy, giả vờ nó không tồn tại và thực hiện hai điều mà nó làm thủ công là khá khó xử sự lựa chọn

Trong thực tế, tôi hiếm khi sử dụng Từ điển thô, do câu châm ngôn đơn giản này: chọn lớp / thùng chứa chung chung nhất cung cấp cho bạn chức năng bạn cần . Từ điển không được thiết kế để tra cứu theo giá trị thay vì theo khóa (ví dụ), vì vậy nếu đó là thứ bạn muốn, có thể có ý nghĩa hơn khi sử dụng cấu trúc thay thế. Tôi nghĩ rằng tôi có thể đã sử dụng Từ điển một lần trong dự án phát triển dài hạn mà tôi đã làm, đơn giản vì nó hiếm khi là công cụ phù hợp cho công việc tôi đang cố gắng thực hiện. Từ điển chắc chắn không phải là con dao quân đội Thụy Sĩ của hộp công cụ C #.

Có gì sai với các tham số ra?

CA1021: Tránh các tham số

Mặc dù các giá trị trả về là phổ biến và được sử dụng nhiều, nhưng việc áp dụng đúng các tham số out và ref yêu cầu kỹ năng thiết kế và mã hóa trung gian. Các kiến ​​trúc sư thư viện thiết kế cho một đối tượng chung không nên mong đợi người dùng thành thạo làm việc với các tham số ngoài hoặc ref.

Tôi đoán đó là nơi bạn nghe nói rằng các tham số giống như một mô hình chống. Như với tất cả các quy tắc, bạn nên đọc kỹ hơn để hiểu 'tại sao' và trong trường hợp này thậm chí còn có một đề cập rõ ràng về cách mẫu Thử không vi phạm quy tắc :

Các phương thức triển khai mẫu Thử, chẳng hạn như System.Int32.TryPude, không nêu ra vi phạm này.


Toàn bộ danh sách hướng dẫn là điều tôi ước nhiều người sẽ đọc. Đối với những người theo dõi, đây là gốc rễ của nó. docs.microsoft.com/en-us/visualstudio/code-quality/ trộm
Peter Wone

10

Có ít nhất hai phương pháp bị thiếu trong từ điển C # mà theo tôi, việc dọn sạch mã đáng kể trong rất nhiều tình huống ở các ngôn ngữ khác. Đầu tiên là trả về một Option, cho phép bạn viết mã như sau trong Scala:

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

Thứ hai là trả về giá trị mặc định do người dùng chỉ định nếu không tìm thấy khóa:

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

Có một điều cần nói khi sử dụng các thành ngữ mà ngôn ngữ cung cấp khi thích hợp, như Trymẫu, nhưng điều đó không có nghĩa là bạn phải chỉ sử dụng những gì ngôn ngữ cung cấp. Chúng tôi là lập trình viên. Bạn có thể tạo ra sự trừu tượng mới để làm cho tình huống cụ thể của chúng ta dễ hiểu hơn, đặc biệt là nếu nó loại bỏ rất nhiều sự lặp lại. Nếu bạn thường xuyên cần một cái gì đó, như tra cứu ngược hoặc lặp qua các giá trị, hãy thực hiện nó. Tạo giao diện bạn muốn bạn có.


Tôi thích tùy chọn thứ 2. Có lẽ tôi sẽ phải viết một hoặc hai phương thức mở rộng :)
Adam B

2
trên các phương thức mở rộng c # cộng có nghĩa là bạn có thể tự thực hiện những phương pháp này
jk.

5

Đó là 4 dòng mã về cơ bản để làm như sau: DoSomethingWith(dict["key"])

Tôi đồng ý rằng điều này là không phù hợp. Một cơ chế mà tôi muốn sử dụng trong trường hợp này, trong đó giá trị là kiểu cấu trúc, là:

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

Bây giờ chúng tôi có một phiên bản mới của TryGetValuelợi nhuận đó int?. Sau đó chúng ta có thể thực hiện một mẹo tương tự để mở rộng T?:

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

Và bây giờ đặt nó lại với nhau:

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

và chúng tôi xuống một tuyên bố rõ ràng duy nhất.

Tôi đã nghe nói rằng sử dụng từ khóa out là một kiểu chống vì nó làm cho các hàm biến đổi các tham số của chúng.

Tôi sẽ ít mạnh mẽ hơn một chút, và nói rằng tránh đột biến khi có thể là một ý tưởng tốt.

Tôi thấy mình thường cần một từ điển "đảo ngược", nơi tôi lật các phím và giá trị.

Sau đó thực hiện hoặc có được một từ điển hai chiều. Họ rất đơn giản để viết, hoặc có rất nhiều triển khai có sẵn trên internet. Có một loạt các triển khai ở đây, ví dụ:

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

Tương tự, tôi thường muốn lặp qua các mục trong từ điển và thấy mình chuyển đổi các khóa hoặc giá trị thành danh sách, v.v. để làm điều này tốt hơn.

Chắc chắn, tất cả chúng ta làm.

Tôi cảm thấy gần như luôn luôn có cách sử dụng từ điển tốt hơn, thanh lịch hơn, nhưng tôi cảm thấy hụt hẫng.

Hãy tự hỏi mình "giả sử tôi có một lớp khác ngoài Dictionaryviệc thực hiện thao tác chính xác mà tôi muốn làm; lớp đó sẽ trông như thế nào?" Sau đó, khi bạn đã trả lời câu hỏi đó, hãy thực hiện lớp đó . Bạn là một lập trình viên máy tính. Giải quyết vấn đề của bạn bằng cách viết chương trình máy tính!


Cảm ơn. Bạn có nghĩ rằng bạn có thể giải thích thêm một chút về phương thức hành động thực sự đang làm gì không? Tôi mới bắt đầu hành động.
Adam B

1
@AdamB một hành động là một đối tượng thể hiện khả năng gọi một phương thức void. Như bạn thấy, về phía người gọi, chúng ta truyền tên của một phương thức void có danh sách tham số khớp với các đối số kiểu chung của hành động. Về phía người gọi, hành động được gọi như bất kỳ phương thức nào khác.
Eric Lippert

1
@AdamB: Bạn có thể nghĩ Action<T>là rất giống với interface IAction<T> { void Invoke(T t); }, nhưng với các quy tắc rất khoan dung về những gì "thực hiện" giao diện đó, và về cách Invokecó thể được gọi. Nếu bạn muốn biết thêm, hãy tìm hiểu về "đại biểu" trong C #, sau đó tìm hiểu về biểu thức lambda.
Eric Lippert

Ok tôi nghĩ rằng tôi đã nhận nó. Vì vậy, hành động cho phép bạn đặt một phương thức làm tham số cho DoIt (). Và gọi phương thức đó.
Adam B

@AdamB: Chính xác là đúng. Đây là một ví dụ về "lập trình chức năng với các hàm bậc cao hơn". Hầu hết các hàm lấy dữ liệu làm đối số; các hàm bậc cao hơn lấy các hàm làm đối số và sau đó thực hiện các hàm với các hàm đó. Khi bạn tìm hiểu thêm C #, bạn sẽ thấy LINQ được thực hiện hoàn toàn với các chức năng bậc cao hơn.
Eric Lippert

4

Cấu TryGetValue()trúc chỉ cần thiết nếu bạn không biết liệu "khóa" có xuất hiện dưới dạng khóa trong từ điển hay không, nếu không DoSomethingWith(dict["key"])thì hoàn toàn hợp lệ.

Cách tiếp cận "ít bẩn hơn" có thể được sử dụng ContainsKey()như một kiểm tra thay thế.


6
Cách tiếp cận "ít bẩn hơn" có thể là sử dụng ContainsKey () làm kiểm tra thay thế. " Tôi không đồng ý. Là tối ưu như vậy TryGetValue, ít nhất nó làm cho nó khó quên để xử lý các trường hợp trống. Lý tưởng nhất, tôi muốn điều này sẽ chỉ trả lại một Tùy chọn.
Alexander - Phục hồi

1
Có một vấn đề tiềm ẩn với ContainsKeycách tiếp cận các ứng dụng đa luồng, đó là thời điểm kiểm tra lỗ hổng thời gian sử dụng (TOCTOU). Điều gì nếu một số luồng khác xóa khóa giữa các cuộc gọi đến ContainsKeyGetValue?
David Hammen

2
@JAD Tùy chọn sáng tác. Bạn có thể có mộtOptional<Optional<T>>
Alexander - Phục hồi Monica

2
@DavidHammen Để rõ ràng, bạn sẽ cần TryGetValuetrên ConcurrentDictionary. Từ điển thông thường sẽ không được đồng bộ hóa
Alexander - Tái lập lại

2
@JAD Không, (kiểu thổi tùy chọn của Java, đừng bắt tôi phải bắt đầu). Tôi đang nói một cách trừu tượng hơn
Alexander - Tái lập lại

2

Các câu trả lời khác chứa những điểm tuyệt vời, vì vậy tôi sẽ không trình bày lại chúng ở đây, nhưng thay vào đó tôi sẽ tập trung vào phần này, phần lớn dường như bị bỏ qua cho đến nay:

Tương tự, tôi thường muốn lặp qua các mục trong từ điển và thấy mình chuyển đổi các khóa hoặc giá trị thành danh sách, v.v. để làm điều này tốt hơn.

Trên thực tế, việc lặp lại từ điển khá dễ dàng vì nó thực hiện IEnumerable:

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

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

Nếu bạn thích Linq, điều đó cũng hoạt động tốt:

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

Nói chung, tôi không thấy Từ điển là một phản mẫu - chúng chỉ là một công cụ cụ thể với những cách sử dụng cụ thể.

Ngoài ra, để biết thêm chi tiết cụ thể, hãy kiểm tra SortedDictionary(sử dụng cây RB để có hiệu suất dễ dự đoán hơn) và SortedList(cũng là một từ điển có tên khó hiểu, hy sinh tốc độ chèn để tăng tốc độ tra cứu, nhưng sẽ tỏa sáng nếu bạn nhìn chằm chằm vào một cái cố định, trước bộ sắp xếp). Tôi đã có một trường hợp thay thế một Dictionaryvới một SortedDictionarykết quả là một thứ tự cường độ nhanh thực hiện (nhưng nó có thể xảy ra theo chiều ngược lại quá).


1

Nếu bạn cảm thấy việc sử dụng một từ điển là khó xử thì nó có thể không phải là lựa chọn đúng đắn cho vấn đề của bạn. Từ điển là tuyệt vời nhưng giống như một người bình luận nhận thấy, thường chúng được sử dụng như một lối tắt cho một thứ đáng lẽ phải là một lớp. Hoặc bản thân từ điển có thể đúng như một phương thức lưu trữ cốt lõi nhưng cần có một lớp bao bọc xung quanh nó để cung cấp các phương thức dịch vụ mong muốn.

Rất nhiều điều đã được nói về Dict [key] so với TryGet. Những gì tôi sử dụng rất nhiều là lặp lại từ điển bằng cách sử dụng KeyValuePair. Rõ ràng đây là một công trình ít được biết đến.

Lợi ích chính của một từ điển là nó thực sự nhanh so với các bộ sưu tập khác khi số lượng vật phẩm tăng lên. Nếu bạn phải kiểm tra xem một khóa có tồn tại nhiều không, bạn có thể tự hỏi mình xem việc sử dụng từ điển của bạn có phù hợp không. Là một khách hàng, bạn thường nên biết những gì bạn đặt trong đó và do đó những gì sẽ an toàn để truy vấn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.