Effective C# 原則23:避免返回內部類對象的引用(翻譯)

來源:互聯網
上載者:User

Effective C# 原則23:避免返回內部類對象的引用

Item 23: Avoid Returning References to Internal Class Objects

你已經知道,所謂的唯讀屬性就是指調用者無法修改這個屬性。不幸運的是,這並不是一直有效。如果你建立了一個屬性,它返回一個參考型別,那麼調用者就可以訪問這個對象的公用成員,也包括修改這些屬性的狀態。例如:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataSet Data
  {
    get
    {
      return _ds;
    }
  }
}

// Access the dataset:
DataSet ds = bizObj.Data;
// Not intended, but allowed:
ds.Tables.Clear( ); // Deletes all data tables.

任何MyBusinessObject的公用客戶都可以修改你的內部dateset。你建立的屬性用來隱藏類的內部資料結構,你提供了方法,讓知道該方法的客戶熟練的操作資料。因此,你的類可以管理內部狀態的任何改變。然而,唯讀屬性對於類的封裝來說開了一個後門。當你考慮這些問題時,它並不是一個可讀可寫屬性,而是一個唯讀屬性。

歡迎來到一個精彩的基於引用的系統,任何返回引用的成員都會返回一個對象的控制代碼。你給了調用者一個介面的控制代碼,因此調用者修改這個對象的某個內部引用時,不再需要通過這個對象。

很清楚,你想防止這樣的事情發生。你為你的類建立了一個介面,同時希望使用者使用這個介面。你不希望使用者在不明白你的意圖時,訪問並修改對象的內部狀態。你有四個策略來保護你的內部資料結構不被無意的修改:實值型別,恒定類型,介面和封裝(模式)。

實值型別在通過屬性訪問時,是資料的拷貝。客戶對類的拷貝資料所做的任何修改,不會影響到對象的內部狀態。客戶可以根據需求隨意的修改拷貝的資料。這對你的內部狀態沒有任意影響。

恒定類型,例如System.String,也是安全的。你可以返回一個字串,或者其它恒定類型。恒定類型的安全性告訴你,沒有客戶可以修改字串。你的內部狀態是安全的。

第三個選擇就是定義介面,從而充許客戶訪問內部成員的部份功能(參見原則19)。當你建立一個自己的類時,你可以建立一些設定介面,用來支援對類的子物件進行設定。通過這些介面來暴露一些功能函數,你可以儘可能的減少一些對資料的無意修改。客戶可以通過你提供的介面訪問類的內部對象,而這個介面並不包含這個類的全部的功能。在DataSet上暴露一個IListsource介面就是這種策略,可以阻止一些有想法的程式員來猜測實現這個介面的對象,以及強制轉換。這樣做和程式員付出更多的工作以及發現更多的BUG都是自找的(譯註:這一句理解可能完全不對,讀者可以自行參考原文:But programmers who go to that much work to create bugs get what they deserve.)。

System.Dataset類同時也使用了最後一種策略:封裝對象。DataViewManager類提供了一種訪問DataSet的方法,而且防止變向的方法來訪問DataSeto類:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataView this[ string tableName ]
  {
    get
    {
      return _ds.DefaultViewManager.
        CreateDataView( _ds.Tables[ tableName ] );
    }
  }
}

// Access the dataset:
DataView list = bizObj[ "customers" ];
foreach ( DataRowView r in list )
  Console.WriteLine( r[ "name" ] );

DataViewManager建立DataView來訪問DataSet裡的個別資料表。DataViewManager沒有提供任何方法來修改DataSet裡的資料表。每一個DataView可以被配置為許可修改個別資料元素,但客戶不能修改資料表,或者資料表的列。讀/寫是預設的,因此客戶還是可以添加,修改,或者刪除個別的資料條目。

在我們開始討論如何建立一個完全唯讀資料檢視時以前,讓我先簡單的瞭解一下你應該如何響應公用使用者的修改。這是很重要的,因為你可能經常要暴露一個DataView給UI控制項,這樣使用者就可以編輯資料(參見原則38)。確信你已經使用過Windows表單的資料繫結,用來給使用者提供對象私人資料編輯。DataSet裡的DataTable引發一些事件,這樣就可以很容易的實現觀查者模式:你的類可以響應其它客戶的任何修改。DataSet裡的DataTable對象會在資料表的任何列以及行發生改變時引發事件。ColumnChanging和RowChanging事件會在編輯的資料提交到DataSet前被引發。而ColumnChanged和RowChanged事件則是在修改提交後引發。

任何時候,當你期望給公用客戶提供修改內部資料的方法時,都可以擴充這樣的技術,但你要驗證而且響應這些改變。你的類應該對內部資料結構產生的事件做一些描述。事件控制代碼通過更新這些內部的狀態來驗證和響應改變。

回到原來的問題上,你想讓客戶查看你的資料,但不許做任何的修改。當你的資料存放區在一個DataSet裡時,你可以通過強制在DataTable上建立一個DataView來防止任何的修改。DataView類包含一些屬性,通過定義這些屬性,可以讓DataView支援在實際的表上添加,刪除,修改甚至是排序。你可以在被請求的DataTable上使用索引器,通過建立一個索引器來返回一個自訂的DataView:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public IList this[ string tableName ]
  {
    get
    {
      DataView view =
        _ds.DefaultViewManager.CreateDataView
        ( _ds.Tables[ tableName ] );
      view.AllowNew = false;
      view.AllowDelete = false;
      view.AllowEdit = false;
      return view;
    }
  }
}

// Access the dataset:
    IList dv = bizOjb[ "customers" ];
    foreach ( DataRowView r in dv )
      Console.WriteLine( r[ "name" ] );

這個類的最後一點摘錄(的代碼)通過訪問IList介面引用,返回這個實際資料表上的視圖。你可以在任何的集合上使用IList介面,並不僅限於DataSet。你不應該只是簡單的返回DataView對象。使用者可以再次簡單的取得編輯,添加/刪除的能力。你返回的視圖已經是自訂的,它不許可在列表的對象上做任何的修改。返回的IList指標確保客戶沒有像DataView對象裡賦於的修改權利。

從公用介面上暴露給使用者的參考型別,可以讓使用者修改對象內部成員,而不用訪問該對象。這看上去不可思議,也會產生一些錯誤。你須要修改類的介面,重新考慮你所暴露的是引用而不是實值型別。如果你只是簡單的返回內部資料,你就給了別人機會去訪問內部成員。你的客戶可以調用成員上任何可用的方法。你可以通過暴露介面來限制一些內部私人資料訪問,或者封裝對象。當你希望你的客戶可以修改你的內部資料時,你應該實現你自己的觀察者模式,這樣你的對象可以驗證修改或者響應它們。
================================

   

Item 23: Avoid Returning References to Internal Class Objects
You'd like to think that a read-only property is read-only and that callers can't modify it. Unfortunately, that's not always the way it works. If you create a property that returns a reference type, the caller can access any public member of that object, including those that modify the state of the property. For example:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataSet Data
  {
    get
    {
      return _ds;
    }
  }
}

// Access the dataset:
DataSet ds = bizObj.Data;
// Not intended, but allowed:
ds.Tables.Clear( ); // Deletes all data tables.

 

Any public client of MyBusinessObject can modify your internal dataset. You created properties to hide your internal data structures. You provided methods to let clients manipulate the data only through known methods, so your class can manage any changes to internal state. And then a read-only property opens a gaping hole in your class encapsulation. It's not a read-write property, where you would consider these issues, but a read-only property.

Welcome to the wonderful world of reference-based systems. Any member that returns a reference type returns a handle to that object. You gave the caller a handle to your internal structures, so the caller no longer needs to go through your object to modify that contained reference.

Clearly, you want to prevent this kind of behavior. You built the interface to your class, and you want users to follow it. You don't want users to access or modify the internal state of your objects without your knowledge. You've got four different strategies for protecting your internal data structures from unintended modifications: value types, immutable types, interfaces, and wrappers.

Value types are copied when clients access them through a property. Any changes to the copy retrieved by the clients of your class do not affect your object's internal state. Clients can change the copy as much as necessary to achieve their purpose. This does not affect your internal state.

Immutable types, such as System.String, are also safe. You can return strings, or any immutable type, safely knowing that no client of your class can modify the string. Your internal state is safe.

The third option is to define interfaces that allow clients to access a subset of your internal member's functionality (see Item 19). When you create your own classes, you can create sets of interfaces that support subsets of the functionality of your class. By exposing the functionality through those interfaces, you minimize the possibility that your internal data changes in ways you did not intend. Clients can access the internal object through the interface you supplied, which will not include the full functionality of the class. Exposing the IListsource interface pointer in the DataSet is one example of this strategy. The Machiavellian programmers out there can defeat that by guessing the type of the object that implements the interface and using a cast. But programmers who go to that much work to create bugs get what they deserve.

The System.Dataset class also uses the last strategy: wrapper objects. The DataViewManager class provides a way to access the DataSet but prevents the mutator methods available through the DataSet class:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataView this[ string tableName ]
  {
    get
    {
      return _ds.DefaultViewManager.
        CreateDataView( _ds.Tables[ tableName ] );
    }
  }
}

// Access the dataset:
DataView list = bizObj[ "customers" ];
foreach ( DataRowView r in list )
  Console.WriteLine( r[ "name" ] );

 

The DataViewManager creates DataViews to access individual data tables in the DataSet. There is no way for the user of your class to modify the tables in your DataSet through the DataViewManager. Each DataView can be configured to allow the modification of individual data elements. But the client cannot change the tables or columns of data. Read/write is the default, so clients can still add, modify, or delete individual items.

Before we talk about how to create a completely read-only view of the data, let's take a brief look at how you can respond to changes in your data when you allow public clients to modify it. This is important because you'll often want to export a DataView to UI controls so that the user can edit the data (see Item 38). You've undoubtedly already used Windows forms data binding to provide the means for your users to edit private data in your objects. The DataTable class, inside the DataSet, raises events that make it easy to implement the observer pattern: Your classes can respond to any changes that other clients of your class have made. The DataTable objects inside your DataSet will raise events when any column or row changes in that table. The ColumnChanging and RowChanging events are raised before an edit is committed to the DataTable. The ColumnChanged and RowChanged events are raised after the change is committed.

You can generalize this technique anytime you want to expose internal data elements for modification by public clients, but you need to validate and respond to those changes. Your class subscribes to events generated by your internal data structure. Event handlers validate changes or respond to those changes by updating other internal state.

Going back to the original problem, you want to let clients view your data but not make any changes. When your data is stored in a DataSet, you can enforce that by creating a DataView for a table that does not allow any changes. The DataView class contains properties that let you customize support for add, delete, modification, or even sorting of the particular table. You can create an indexer to return a customized DataView on the requested table using an indexer:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public IList this[ string tableName ]
  {
    get
    {
      DataView view =
        _ds.DefaultViewManager.CreateDataView
        ( _ds.Tables[ tableName ] );
      view.AllowNew = false;
      view.AllowDelete = false;
      view.AllowEdit = false;
      return view;
    }
  }
}

// Access the dataset:
    IList dv = bizOjb[ "customers" ];
    foreach ( DataRowView r in dv )
      Console.WriteLine( r[ "name" ] );

 

This final excerpt of the class returns the view into a particular data table through its IList interface reference. You can use the IList interface with any collection; it's not specific to the DataSet. You should not simply return the DataView object. Users could easily enable the editing and add/delete capability again. The view you are returning has been customized to disallow any modifications to the objects in the list. Returning the IList pointer keeps clients from modifying the rights they have been given to the DataView object.

Exposing reference types through your public interface allows users of your object to modify its internals without going through the methods and properties you've defined. That seems counterintuitive, which makes it a common mistake. You need to modify your class's interfaces to take into account that you are exporting references rather than values. If you simply return internal data, you've given access to those contained members. Your clients can call any method that is available in your members. You limit that access by exposing private internal data using interfaces, or wrapper objects. When you do want your clients to modify your internal data elements, you should implement the Observer pattern so that your objects can validate changes or respond to them.
 
   

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.