Phát hiện máy tính nhà nước IEn Countable


17

Tôi vừa đọc một bài viết thú vị có tên Trở nên quá dễ thương với lợi nhuận của c #

Nó khiến tôi tự hỏi cách tốt nhất là phát hiện xem IEnumerable là một bộ sưu tập có thể đếm được thực sự hay nếu đó là một máy trạng thái được tạo bằng từ khóa suất.

Ví dụ: bạn có thể sửa đổi DoubleXValue (từ bài viết) thành một cái gì đó như:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

Câu hỏi 1) Có cách nào tốt hơn để làm điều này không?

Câu hỏi 2) Đây có phải là điều tôi nên lo lắng khi tạo API không?


1
Bạn nên sử dụng một giao diện chứ không phải cụ thể và cũng được đặt xuống thấp hơn, ICollection<T>sẽ là lựa chọn tốt hơn vì không phải tất cả các bộ sưu tập đều như vậy List<T>. Ví dụ, mảng Point[]thực hiện IList<T>nhưng không List<T>.
Trevor Pilley

3
Chào mừng bạn đến vấn đề với các loại đột biến. Ienumerable được coi là một kiểu dữ liệu bất biến, do đó việc triển khai đột biến là vi phạm LSP. Bài viết này là một lời giải thích hoàn hảo về sự nguy hiểm của tác dụng phụ, vì vậy hãy thực hành viết các loại C # của bạn để có tác dụng phụ miễn phí nhất có thể và bạn không bao giờ cần phải lo lắng về điều này.
Jimmy Hoffa

1
@JimmyHoffa IEnumerablelà bất biến theo nghĩa là bạn không thể sửa đổi bộ sưu tập (thêm / xóa) trong khi liệt kê nó, tuy nhiên nếu các đối tượng trong danh sách có thể thay đổi, thì việc sửa đổi danh sách đó là hoàn toàn hợp lý.
Trevor Pilley

@TrevorPilley Tôi không đồng ý. Nếu bộ sưu tập được dự đoán là bất biến, kỳ vọng từ người tiêu dùng là các thành viên sẽ không bị đột biến bởi các tác nhân bên ngoài, điều đó sẽ được gọi là tác dụng phụ và vi phạm kỳ vọng bất biến. Vi phạm Pola và giả định LSP.
Jimmy Hoffa

Làm thế nào về việc sử dụng ToList? msdn.microsoft.com/en-us/l Library / bb342261.aspx Nó không phát hiện xem nó có được tạo ra bởi năng suất hay không, nhưng thay vào đó, nó làm cho nó không liên quan.
luiscubal

Câu trả lời:


22

Câu hỏi của bạn, theo tôi hiểu, dường như được dựa trên một tiền đề không chính xác. Hãy để tôi xem nếu tôi có thể xây dựng lại lý do:

  • Bài viết được liên kết mô tả cách các chuỗi được tạo tự động thể hiện hành vi "lười biếng" và cho thấy cách điều này có thể dẫn đến kết quả phản trực quan.
  • Do đó, tôi có thể phát hiện xem một phiên bản cụ thể của IEnumerable sẽ thể hiện hành vi lười biếng này hay không bằng cách kiểm tra xem liệu nó có được tạo tự động hay không.
  • Làm thế nào để làm điều đó?

Vấn đề là tiền đề thứ hai là sai. Ngay cả khi bạn có thể phát hiện xem một IEnumerable cụ thể có phải là kết quả của phép chuyển đổi khối lặp hay không (và vâng, có nhiều cách để làm điều đó) sẽ không có ích vì giả định là sai. Hãy minh họa tại sao.

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

Được rồi, chúng tôi có bốn phương pháp. S1 và S2 là các chuỗi được tạo tự động; S3 và S4 là các chuỗi được tạo thủ công. Bây giờ giả sử chúng ta có:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

Kết quả cho S1 và S4 sẽ là 0; mỗi khi bạn liệt kê chuỗi, bạn sẽ có một tham chiếu mới đến một M được tạo. Kết quả cho S2 và S3 sẽ là 100; mỗi khi bạn liệt kê trình tự, bạn sẽ có cùng tham chiếu đến M bạn đã nhận được lần cuối cùng. Liệu mã trình tự có được tạo tự động hay không trực giao với câu hỏi liệu các đối tượng được liệt kê có nhận dạng tham chiếu hay không. Hai thuộc tính đó - tạo tự động và nhận dạng tham chiếu - thực sự không liên quan gì đến nhau. Bài viết bạn liên kết để xúi giục họ phần nào.

Trừ khi một nhà cung cấp trình tự được ghi nhận là luôn luôn khuyến khích các đối tượng có danh tính tham chiếu , sẽ không khôn ngoan khi cho rằng nó làm như vậy.


2
Chúng tôi đều biết bạn sẽ có câu trả lời chính xác ở đây, đó là một câu trả lời; nhưng nếu bạn có thể đặt thêm một chút định nghĩa vào thuật ngữ "danh tính tham chiếu" thì có thể tốt, tôi đang đọc nó là "bất biến" nhưng đó là vì tôi đã kết luận bất biến trong C # với đối tượng mới trả về mọi loại giá trị hoặc giá trị mà tôi nghĩ là điều tương tự bạn đang đề cập đến. Mặc dù được cấp quyền bất biến có thể có nhiều cách khác nhau, nhưng đó là cách hợp lý duy nhất trong C # (mà tôi biết, bạn có thể biết những cách tôi không biết).
Jimmy Hoffa

2
@JimmyHoffa: Hai tham chiếu đối tượng x và y có "danh tính tham chiếu" nếu Object.ReferenceEquals (x, y) trả về true.
Eric Lippert

Luôn luôn tốt đẹp để có được một phản hồi trực tiếp từ nguồn. Cảm ơn Eric.
Điều

10

Tôi nghĩ khái niệm chính là bạn nên coi bất kỳ IEnumerable<T>bộ sưu tập nào là bất biến : bạn không nên sửa đổi các đối tượng từ nó, bạn nên tạo một bộ sưu tập mới với các đối tượng mới:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(Hoặc viết tương tự ngắn gọn hơn bằng cách sử dụng LINQ Select().)

Nếu bạn thực sự muốn sửa đổi các mục trong bộ sưu tập, không sử dụng IEnumerable<T>, sử dụng IList<T>(hoặc có thể IReadOnlyList<T>nếu bạn không muốn sửa đổi chính bộ sưu tập đó, chỉ các mục trong đó và bạn đang trên .Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

Bằng cách này, nếu ai đó cố gắng sử dụng phương thức của bạn IEnumerable<T>, nó sẽ thất bại tại thời gian biên dịch.


1
Chìa khóa thực sự giống như bạn đã nói, trả về một vô số mới với các sửa đổi khi bạn muốn thay đổi một cái gì đó. Vô số là một loại hoàn toàn khả thi và không có lý do gì để thay đổi, đó chỉ là kỹ thuật sử dụng nó cần được hiểu như bạn đã nói. +1 để đề cập đến cách làm việc với các loại không thay đổi.
Jimmy Hoffa

1
Một số thứ liên quan - bạn cũng nên đối xử với mọi IE như thể nó được thực hiện thông qua thực thi hoãn lại. Điều có thể đúng tại thời điểm bạn truy xuất tài liệu tham khảo IEn của bạn có thể không đúng khi bạn thực sự liệt kê thông qua nó. Ví dụ, trả về một câu lệnh LINQ bên trong khóa có thể dẫn đến một số kết quả bất ngờ thú vị.
Dan Lyons

@JimmyHoffa: Tôi đồng ý. Tôi tiến thêm một bước và cố gắng tránh lặp đi lặp lại IEnumerablenhiều lần. Sự cần thiết phải làm nhiều hơn một lần có thể được giảm bớt bằng cách gọi ToList()và lặp lại danh sách, mặc dù trong trường hợp cụ thể được thể hiện bởi OP, tôi thích giải pháp của Doubleick của Doubleick cũng trả lại IEnumerable. Tất nhiên, trong mã thực tôi có thể sẽ sử dụng LINQđể tạo ra loại này IEnumerable. Tuy nhiên, lựa chọn của Svick yieldcó ý nghĩa trong bối cảnh câu hỏi của OP.
Brian

@Brian Vâng, tôi không muốn thay đổi mã quá nhiều để đưa ra quan điểm của mình. Trong mã thực, tôi sẽ sử dụng Select(). Và liên quan đến nhiều lần lặp lại của IEnumerable: ReSharper rất hữu ích với điều đó, bởi vì nó có thể cảnh báo bạn khi bạn làm điều đó.
Svick

5

Cá nhân tôi nghĩ rằng vấn đề được nhấn mạnh trong bài viết đó bắt nguồn từ việc lạm dụng IEnumerablegiao diện của nhiều nhà phát triển C # ngày nay. Nó dường như đã trở thành loại mặc định khi bạn yêu cầu một bộ sưu tập các đối tượng - nhưng có rất nhiều thông tin IEnumerableđơn giản là không cho bạn biết ... điều quan trọng: liệu nó có được thống nhất * hay không.

Nếu bạn thấy rằng bạn cần phải phát hiện xem một IEnumerablethực sự là một IEnumerablecái gì đó hay cái gì khác, sự trừu tượng đã bị rò rỉ và có lẽ bạn đang ở trong một tình huống mà loại tham số của bạn quá lỏng lẻo. Nếu bạn yêu cầu thêm thông tin mà IEnumerablemột mình không cung cấp, hãy làm rõ yêu cầu đó bằng cách chọn một trong các giao diện khác như ICollectionhoặc IList.

Chỉnh sửa cho các downvoters Vui lòng quay lại và đọc câu hỏi cẩn thận. OP đang yêu cầu một cách để đảm bảo rằng các IEnumerablephiên bản đã được cụ thể hóa trước khi được chuyển sang phương thức API của anh ấy. Câu trả lời của tôi giải quyết câu hỏi đó. Có một vấn đề rộng hơn liên quan đến cách sử dụng phù hợp IEnumerablevà chắc chắn những kỳ vọng về mã mà OP đã dán dựa trên sự hiểu lầm về vấn đề rộng hơn đó, nhưng OP không hỏi cách sử dụng chính xác IEnumerablehoặc cách làm việc với các loại không thay đổi . Thật không công bằng khi hạ thấp tôi vì đã không trả lời một câu hỏi không được đặt ra.

* Tôi sử dụng thuật ngữ reify, những người khác sử dụng stabilizehoặc materialize. Tôi không nghĩ rằng một quy ước chung đã được cộng đồng chấp nhận.


2
Một vấn đề với ICollectionIListlà chúng có thể thay đổi (hoặc ít nhất là nhìn theo cách đó). IReadOnlyCollectionIReadOnlyListtừ .Net 4.5 giải quyết một số vấn đề đó.
Svick

Hãy giải thích các downvote?
MattDavey

1
Tôi không đồng ý rằng bạn nên thay đổi loại bạn đang sử dụng. Ienumerable là một loại tuyệt vời và nó thường có thể có lợi ích của sự bất biến. Tôi nghĩ vấn đề thực sự là mọi người không hiểu cách làm việc với các loại bất biến trong C #. Không có gì đáng ngạc nhiên, đó là một ngôn ngữ cực kỳ trạng thái, vì vậy đây là một khái niệm mới đối với nhiều nhà phát triển C #.
Jimmy Hoffa

Chỉ có ienumerable mới cho phép thực hiện hoãn lại, do đó, không cho phép nó làm tham số loại bỏ toàn bộ một tính năng của C #
Jimmy Hoffa

2
Tôi đã bỏ phiếu vì tôi nghĩ nó sai. Tôi nghĩ đó là theo tinh thần của SE, để đảm bảo mọi người không đưa thông tin không chính xác vào tất cả chúng ta để bỏ phiếu cho những câu trả lời đó. Có lẽ tôi sai, hoàn toàn có thể là tôi sai và bạn đúng. Điều đó tùy thuộc vào cộng đồng rộng lớn hơn để quyết định bằng cách bỏ phiếu. Xin lỗi, bạn đang sử dụng nó một cách cá nhân, nếu đó là bất kỳ sự an ủi nào, tôi đã có nhiều câu trả lời bỏ phiếu tiêu cực tôi đã viết và xóa chung vì tôi chấp nhận cộng đồng nghĩ rằng điều đó là sai nên tôi không thể đúng.
Jimmy Hoffa
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.