Làm thế nào để bạn ngăn IDis Dùng lan rộng đến tất cả các lớp của bạn?


138

Bắt đầu với những lớp đơn giản này ...

Hãy nói rằng tôi có một tập hợp các lớp đơn giản như thế này:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A Buscó a Driver, Drivercó hai Shoes, mỗi Shoecó a Shoelace. Tất cả đều rất ngớ ngẩn.

Thêm một đối tượng IDis Dùng cho Shoelace

Sau đó tôi quyết định rằng một số thao tác trên Shoelacecó thể là đa luồng, vì vậy tôi thêm một EventWaitHandlecho các luồng để giao tiếp. Vì vậy, Shoelacebây giờ trông như thế này:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

Triển khai IDis Dùng trên Dây giày

Nhưng bây giờ FxCop của Microsoft sẽ phàn nàn: "Triển khai IDis Dùng trên 'Dây giày' vì nó tạo ra các thành viên của các loại IDis Dùng sau: 'EventWaitHandle'."

Được rồi, tôi thực hiện IDisposabletrên Shoelacevà lớp nhỏ gọn của tôi trở nên lộn xộn khủng khiếp này:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

Hoặc (như được chỉ ra bởi các nhà bình luận) vì Shoelacebản thân nó không có tài nguyên không được quản lý, tôi có thể sử dụng triển khai xử lý đơn giản hơn mà không cần Dispose(bool)và Destructor:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

Xem trong kinh dị khi lây lan IDis Dùng

Đúng là đã sửa. Nhưng bây giờ FxCop sẽ phàn nàn rằng Shoetạo ra một Shoelace, vì vậy Shoecũng phải như IDisposablevậy.

Drivertạo ra Shoenhư vậy Driverphải được IDisposable. Và Bustạo ra Drivernhư vậy Busphải được IDisposablevà như vậy.

Đột nhiên, sự thay đổi nhỏ của tôi Shoelacegây ra cho tôi rất nhiều công việc và ông chủ của tôi đang tự hỏi tại sao tôi cần phải kiểm tra Busđể thực hiện một thay đổi Shoelace.

Câu hỏi

Làm thế nào để bạn ngăn chặn sự lây lan này IDisposable, nhưng vẫn đảm bảo rằng các đối tượng không được quản lý của bạn được xử lý đúng cách?


9
Một câu hỏi đặc biệt hay, tôi tin rằng câu trả lời là giảm thiểu việc sử dụng chúng và cố gắng duy trì IDisposeables ở mức độ cao khi sử dụng, nhưng điều này không phải lúc nào cũng có thể (đặc biệt là khi những IDisposeables đó là do xen kẽ với các dll C ++ hoặc tương tự). Nhìn fwd để trả lời.
annakata

Được rồi Dan, tôi đã cập nhật câu hỏi để hiển thị cả hai phương pháp triển khai IDis Dùng trên Shoelace.
GrahamS

Tôi thường cảnh giác dựa vào các chi tiết triển khai của các lớp khác để bảo vệ tôi. Không có điểm nào mạo hiểm nếu tôi có thể dễ dàng ngăn chặn nó. Có thể tôi quá thận trọng hoặc có thể tôi chỉ mất quá nhiều thời gian với tư cách là một lập trình viên C, nhưng tôi muốn sử dụng cách tiếp cận của Ailen: "Để chắc chắn, chắc chắn" :)
GrahamS

@Dan: Kiểm tra null vẫn cần thiết để đảm bảo rằng chính đối tượng chưa được đặt thành null, trong trường hợp đó, lệnh gọi WaitHandle.Dispose () sẽ ném NullReferenceException.
Scott Dorman

1
Dù thế nào đi nữa, bạn thực sự vẫn nên sử dụng phương pháp Vứt bỏ (bool) như trong phần "Thực hiện IDis trên dây giày" của bạn vì đó (trừ phần hoàn thiện) là mẫu đầy đủ. Chỉ vì một lớp ngụ ý IDis Dùng không có nghĩa là nó cần một bộ hoàn thiện.
Scott Dorman

Câu trả lời:


36

Bạn thực sự không thể "ngăn chặn" IDis Dùng lan rộng. Một số lớp cần phải được xử lý, như AutoResetEvent, và cách hiệu quả nhất là thực hiện nó trong Dispose()phương thức để tránh chi phí chung của người hoàn thiện. Nhưng phương thức này phải được gọi bằng cách nào đó, vì vậy, chính xác như trong ví dụ của bạn, các lớp đóng gói hoặc chứa IDis Dùng phải loại bỏ chúng, vì vậy chúng cũng phải dùng một lần, v.v. Cách duy nhất để tránh nó là:

  • tránh sử dụng các lớp IDis Dùng một lần nếu có thể, khóa hoặc chờ các sự kiện ở những nơi duy nhất, giữ các tài nguyên đắt tiền ở một nơi, v.v.
  • chỉ tạo chúng khi bạn cần và loại bỏ chúng ngay sau đó ( usingmẫu)

Trong một số trường hợp, IDis Dùng có thể được bỏ qua vì nó hỗ trợ một trường hợp tùy chọn. Ví dụ: WaitHandle triển khai IDis Dùng để hỗ trợ Mutex có tên. Nếu tên không được sử dụng, phương thức Vứt bỏ không làm gì cả. MemoryStream là một ví dụ khác, nó không sử dụng tài nguyên hệ thống và việc triển khai Vứt bỏ của nó cũng không làm gì cả. Suy nghĩ cẩn thận về việc một tài nguyên không được quản lý đang được sử dụng hay không có thể được hướng dẫn. Vì vậy, có thể kiểm tra các nguồn có sẵn cho các thư viện .net hoặc sử dụng trình dịch ngược.


4
Lời khuyên mà bạn có thể bỏ qua vì việc triển khai hôm nay không làm gì xấu cả, vì phiên bản trong tương lai thực sự có thể làm điều gì đó quan trọng trong Vứt bỏ, và bây giờ bạn khó theo dõi rò rỉ.
Andy

1
"Giữ tài nguyên đắt tiền", IDis Dùng không nhất thiết phải đắt tiền, thực sự ví dụ này sử dụng tay cầm chờ thậm chí là về "trọng lượng nhẹ" như bạn nhận được.
markmnl

20

Về tính chính xác, bạn không thể ngăn chặn sự lây lan của IDis Dùng thông qua mối quan hệ đối tượng nếu một đối tượng cha mẹ tạo và về cơ bản sở hữu một đối tượng con mà bây giờ phải dùng một lần. FxCop là chính xác trong tình huống này và cha mẹ phải là IDis Dùng.

Những gì bạn có thể làm là tránh thêm IDis Dùng một lớp lá trong hệ thống phân cấp đối tượng của bạn. Đây không phải lúc nào cũng là một nhiệm vụ dễ dàng nhưng nó là một bài tập thú vị. Từ góc độ logic, không có lý do gì mà ShoeLace cần phải dùng một lần. Thay vì thêm WaitHandle ở đây, bạn cũng có thể thêm liên kết giữa ShoeLace và WaitHandle tại điểm được sử dụng. Cách đơn giản nhất là thông qua một ví dụ từ điển.

Nếu bạn có thể di chuyển WaitHandle thành một liên kết lỏng lẻo thông qua bản đồ tại điểm WaitHandle thực sự được sử dụng thì bạn có thể phá vỡ chuỗi này.


2
Nó cảm thấy rất kỳ lạ để làm cho một hiệp hội ở đó. AutoResetEvent này là riêng tư đối với việc triển khai Shoelace, vì vậy theo tôi, việc phơi bày công khai sẽ là sai.
GrahamS

@GrahamS, tôi không nói phơi bày công khai. Tôi đang nói nó có thể được di chuyển đến điểm buộc dây giày không. Có lẽ nếu buộc dây giày là một nhiệm vụ phức tạp như vậy thì chúng phải là một lớp dây giày.
JaredPar

1
Bạn có thể mở rộng câu trả lời của mình với một số đoạn mã JaredPar không? Tôi có thể kéo dài ví dụ của mình một chút nhưng tôi đang tưởng tượng rằng Shoelace tạo và bắt đầu luồng Tyer, nó kiên nhẫn chờ đợi tại WaitHandle.WaitOne () Shoelace sẽ gọi WaitHandle.Set () khi nó muốn luồng bắt đầu.
GrahamS

1
+1 về điều này. Tôi nghĩ thật lạ khi chỉ Shoelace sẽ được gọi đồng thời chứ không phải những người khác. Hãy nghĩ về những gì nên là gốc tổng hợp. Trong trường hợp này phải là Bus, IMHO, mặc dù tôi không quen với tên miền. Vì vậy, trong trường hợp đó, xe buýt nên chứa waithandle và tất cả các hoạt động chống lại xe buýt và tất cả trẻ em của nó sẽ được đồng bộ hóa.
Julian Gustafsson

16

Để ngăn chặn sự IDisposablelây lan, bạn nên cố gắng gói gọn việc sử dụng một vật thể dùng một lần bên trong một phương thức duy nhất. Cố gắng thiết kế Shoelacekhác nhau:

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

Phạm vi của xử lý chờ được giới hạn trong Tiephương thức và lớp không cần phải có trường dùng một lần và vì vậy sẽ không cần phải dùng một lần.

Vì tay cầm chờ là một chi tiết triển khai bên trong Shoelace, nên nó không thay đổi theo bất kỳ cách nào giao diện công khai của nó, như thêm một giao diện mới trong khai báo. Điều gì sẽ xảy ra sau đó khi bạn không cần một trường dùng một lần nữa, bạn sẽ xóa phần IDisposablekhai báo chứ? Nếu bạn nghĩ về sự Shoelace trừu tượng , bạn sẽ nhanh chóng nhận ra rằng nó không nên bị ô nhiễm bởi các phụ thuộc cơ sở hạ tầng, như thế nào IDisposable. IDisposablenên được dành riêng cho các lớp có sự trừu tượng hóa đóng gói một tài nguyên yêu cầu dọn dẹp xác định; tức là, đối với các lớp trong đó tính định đoạt là một phần của sự trừu tượng hóa .


1
Tôi hoàn toàn đồng ý rằng chi tiết triển khai của Shoelace không nên gây ô nhiễm giao diện công cộng, nhưng quan điểm của tôi là có thể khó tránh. Những gì bạn đề xuất ở đây sẽ không phải lúc nào cũng có thể: mục đích của AutoResetEvent () là để giao tiếp giữa các luồng, vì vậy phạm vi của nó thường sẽ mở rộng ra ngoài một phương thức.
GrahamS

@GrahamS: đó là lý do tại sao tôi nói cố gắng thiết kế theo cách đó. Bạn cũng có thể vượt qua dùng một lần cho các phương pháp khác. Nó chỉ bị hỏng khi các cuộc gọi bên ngoài đến lớp điều khiển vòng đời của thiết bị dùng một lần. Tôi sẽ cập nhật câu trả lời phù hợp.
Jordão

2
xin lỗi, tôi biết bạn có thể vượt qua dùng một lần, nhưng tôi vẫn không thấy nó hoạt động. Trong ví dụ của tôi, AutoResetEventnó được sử dụng để giao tiếp giữa các luồng khác nhau hoạt động trong cùng một lớp, vì vậy nó phải là một biến thành viên. Bạn không thể giới hạn phạm vi của nó cho một phương thức. (ví dụ, hãy tưởng tượng rằng một luồng chỉ chờ đợi một số công việc bằng cách chặn waitHandle.WaitOne(). Chuỗi chính sau đó gọi shoelace.Tie()phương thức, nó chỉ thực hiện một waitHandle.Set()và trả về ngay lập tức).
GrahamS

3

Về cơ bản, điều này xảy ra khi bạn trộn Thành phần hoặc Tập hợp với các lớp dùng một lần. Như đã đề cập, lối thoát đầu tiên sẽ là tái cấu trúc bộ phận chờ ra khỏi dây giày.

Có nói rằng, bạn có thể loại bỏ mô hình Dùng một lần đáng kể khi bạn không có tài nguyên không được quản lý. (Tôi vẫn đang tìm kiếm một tài liệu tham khảo chính thức cho việc này.)

Nhưng bạn có thể bỏ qua hàm hủy và GC.SuppressFinalize (cái này); và có thể dọn sạch khoảng trống ảo Vứt bỏ (bool dispose) một chút.


Cảm ơn Henk. Nếu tôi đã tạo ra WaitHandle ra khỏi Shoelace thì ai đó vẫn phải Vứt bỏ nó ở đâu đó để làm sao ai đó biết rằng Shoelace đã kết thúc với nó (và Shoelace đã không chuyển nó sang bất kỳ lớp nào khác)?
GrahamS

Bạn sẽ cần phải di chuyển không chỉ là Sáng tạo mà còn là 'trách nhiệm đối với' waithandle. Câu trả lời của jaredPar có một khởi đầu thú vị của một ý tưởng.
Henk Holterman

Blog này bao gồm việc triển khai IDis Dùng
PaulS

3

Thật thú vị nếu Driverđược định nghĩa như trên:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

Sau đó, khi Shoeđược thực hiện IDisposable, FxCop (v1.36) không phàn nàn rằng Drivercũng nên IDisposable.

Tuy nhiên nếu nó được định nghĩa như thế này:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

sau đó nó sẽ phàn nàn.

Tôi nghi ngờ rằng đây chỉ là một hạn chế của FxCop, chứ không phải là một giải pháp, bởi vì trong phiên bản đầu tiên, các phiên bản Shoevẫn đang được tạo bởi Drivervà vẫn cần phải được xử lý bằng cách nào đó.


2
Nếu Hiển thị thực hiện IDis Dùng một lần, việc tạo nó trong trình khởi tạo trường là nguy hiểm. Điều thú vị là FXCop sẽ không cho phép khởi tạo như vậy, vì trong C #, cách duy nhất để thực hiện chúng một cách an toàn là với các biến ThreadStatic (hết sức gớm ghiếc). Trong vb.net, có thể khởi tạo các đối tượng IDis Dùng một cách an toàn mà không cần các biến ThreadStatic. Mã vẫn xấu, nhưng không đến nỗi gớm ghiếc. Tôi ước MS sẽ cung cấp một cách tốt đẹp để sử dụng các công cụ khởi tạo trường như vậy một cách an toàn.
supercat

1
@supercat: cảm ơn. Bạn có thể giải thích tại sao nó nguy hiểm? Tôi đoán rằng nếu một ngoại lệ được ném vào một trong các nhà Shoexây dựng thì shoessẽ bị bỏ lại trong tình trạng khó khăn?
GrahamS

3
Trừ khi ai đó biết rằng một loại IDis Dùng cụ thể có thể bị từ bỏ một cách an toàn, người ta phải đảm bảo rằng mọi đối tượng được tạo ra đều bị loại bỏ. Nếu một IDis Dùng được tạo trong trình khởi tạo trường của một đối tượng nhưng một ngoại lệ được đưa ra trước khi đối tượng được xây dựng hoàn chỉnh, đối tượng và tất cả các trường của nó sẽ bị bỏ rơi. Ngay cả khi việc xây dựng đối tượng được bọc trong một phương thức nhà máy sử dụng khối "thử" để phát hiện công trình đó thất bại, nhà máy vẫn khó có được các tham chiếu đến các đối tượng cần xử lý.
supercat

3
Cách duy nhất tôi biết để đối phó với điều đó trong C # là sử dụng biến ThreadStatic để giữ một danh sách các đối tượng sẽ cần xử lý nếu hàm tạo hiện tại ném. Sau đó, mỗi trình khởi tạo trường đăng ký từng đối tượng khi nó được tạo, yêu cầu nhà máy xóa danh sách nếu nó hoàn thành thành công và sử dụng mệnh đề "cuối cùng" để loại bỏ tất cả các mục khỏi danh sách nếu nó bị xóa. Đó sẽ là một cách tiếp cận khả thi, nhưng các biến ThreadStatic thì xấu. Trong vb.net, người ta có thể sử dụng một kỹ thuật tương tự mà không cần bất kỳ biến ThreadStatic nào.
năm11

3

Tôi không nghĩ rằng có một cách kỹ thuật để ngăn chặn IDis Dùng lan rộng nếu bạn giữ thiết kế của mình thật chặt. Sau đó, người ta nên tự hỏi nếu thiết kế là đúng.

Trong ví dụ của bạn, tôi nghĩ thật hợp lý khi để giày sở hữu dây giày, và có lẽ, người lái xe nên sở hữu đôi giày của mình. Tuy nhiên, xe buýt không nên sở hữu tài xế. Thông thường, tài xế xe buýt không theo xe buýt đến bãi phế liệu :) Trong trường hợp tài xế và giày, tài xế hiếm khi tự làm giày, nghĩa là họ không thực sự "sở hữu" chúng.

Một thiết kế thay thế có thể là:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

Thật không may, thiết kế mới phức tạp hơn, vì nó yêu cầu các lớp học thêm để khởi tạo và xử lý các trường hợp cụ thể của giày và trình điều khiển, nhưng sự phức tạp này là cố hữu đối với vấn đề đang được giải quyết. Điều tốt là xe buýt không cần phải dùng một lần chỉ đơn giản cho mục đích xử lý dây giày.


2
Điều này không thực sự giải quyết vấn đề ban đầu mặc dù. Nó có thể giữ cho FxCop im lặng nhưng một cái gì đó vẫn nên loại bỏ Dây giày.
AndrewS

Tôi nghĩ rằng tất cả những gì bạn đã làm là chuyển vấn đề đi nơi khác. ShoePairWithDis DùngLaces hiện sở hữu Shoelace (), do đó, nó cũng phải được tạo IDis Dùng một lần - vậy ai là người vứt bỏ đôi giày? Bạn đang để điều này cho một số container IoC để xử lý?
GrahamS

@AndrewS: Thật vậy, một cái gì đó vẫn phải vứt bỏ các trình điều khiển, giày và dây buộc, nhưng việc xử lý không nên được bắt đầu bằng xe buýt. @GrahamS: Bạn nói đúng, tôi đang chuyển vấn đề đi nơi khác, vì tôi tin rằng nó thuộc về nơi khác. Thông thường, sẽ có một đôi giày bắt đầu lớp, cũng sẽ chịu trách nhiệm cho việc xử lý giày. Tôi cũng đã chỉnh sửa mã để biến ShoePairWithDis Dùng một lần nữa.
Joh

@Joh: Cảm ơn. Như bạn nói, vấn đề với việc di chuyển nó đi nơi khác là bạn tạo ra sự phức tạp hơn rất nhiều. Nếu ShoePairWithDis DùngLaces được tạo bởi một số lớp khác, thì lớp đó có phải được thông báo khi Trình điều khiển kết thúc với Giày của mình không, để nó có thể loại bỏ () chúng đúng cách không?
GrahamS

1
@Graham: có, một cơ chế thông báo nào đó sẽ là cần thiết. Một chính sách xử lý dựa trên số lượng tham chiếu có thể được sử dụng, ví dụ. Có một số phức tạp được thêm vào, nhưng như bạn có thể biết, "Không có thứ gọi là giày miễn phí" :)
Joh

1

Làm thế nào về việc sử dụng Inversion of Control?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

Bây giờ bạn đã biến nó thành vấn đề của người gọi và Shoelace không thể phụ thuộc vào xử lý chờ có sẵn khi cần.
Andy

@andy Ý bạn là gì khi nói "Shoelace không thể phụ thuộc vào tay cầm chờ sẵn khi nó cần"? Nó được chuyển đến cho nhà xây dựng.
Darragh

Một cái gì đó khác có thể đã gây rối với trạng thái tự động phát hiện trước khi đưa nó cho Shoelace, và nó có thể bắt đầu với IS ở trạng thái xấu; một cái gì đó khác có thể làm hỏng trạng thái của IS trong khi Shoelace đang làm điều đó, khiến Shoelace làm điều sai trái. Đó là lý do tương tự bạn chỉ khóa các thành viên tư nhân. "Nói chung, .. tránh bị khóa trong các trường hợp nằm ngoài sự kiểm soát mã của bạn" docs.microsoft.com/en-us/dotnet/csharp/lingu-reference/ phỏng
Andy

Tôi đồng ý chỉ khóa các thành viên tư nhân. Nhưng có vẻ như tay cầm chờ là khác nhau. Trong thực tế, có vẻ hữu ích hơn khi truyền chúng vào đối tượng vì cùng một thể hiện cần được sử dụng để giao tiếp qua các luồng. Tuy nhiên, tôi nghĩ IoC cung cấp một giải pháp hữu ích cho vấn đề của OP.
Darragh

Cho rằng ví dụ của OP có waithandle là một thành viên tư nhân, giả định an toàn là đối tượng mong đợi quyền truy cập độc quyền vào nó. Làm cho tay cầm có thể nhìn thấy bên ngoài ví dụ vi phạm điều đó. Thông thường, một IoC có thể tốt cho những thứ như thế này, nhưng không phải khi nói đến luồng.
Andy
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.