Effective C# 避免返回內部類對象的引用

來源:互聯網
上載者:User

大家可能認為唯讀屬性就只能讀取,調用者不可能更改屬性值。可惜的是,並非所有情況都如此。如果我們建立的屬性返回了一個參考型別,那麼調用者就可以訪問該對象的公有成員,包括那些修改屬性狀態的成員。例如:

 

public class MyBusinessObject

{

// 唯讀屬性提供了對私人資料成員的訪問:

private DataSet _ds;

public DataSet Data

{

    get

    {

      return _ds;

    }

}

}

// 訪問DataSet:

DataSet ds = bizObj.Data;

// 並非我們期望的行為,但是這麼做是允許的:

ds.Tables.Clear( ); // 刪除所有資料表。

 

 

這裡,任何外部的客戶代碼都可以修改MyBusinessObject類型內部的DataSet。我們可以建立屬性來隱藏內部的資料結構,也可以建立方法來讓客戶代碼僅通過它們操作資料,這樣我們的類就可以管理對內部狀態的任何改變。但是一個唯讀屬性卻將這樣的類封裝開啟了一個缺口。由於是唯讀屬性,而不是一個讀/ 寫屬性,因此會出現我們考慮不到的問題。

歡迎大家來到奇妙的基於“引用”的類型系統中來!在這樣的系統中,任何返回參考型別的成員,都會返回該對象的一個控制代碼(handle)。這個控制代碼使得調用者可以到達對象內部的資料結構,無需通過對象就可以改變其中包含的引用。

顯然,我們希望避免這種行為。我們可以選擇為類建立介面,然後讓使用者通過介面使用對象。我們不希望使用者在我們不知道的情況下,訪問或者改變對象的內部狀態。共有4種不同的策略可以防止類型的內部資料結構遭受無意的改變:實值型別、常量類型、介面和封裝器(wrapper)。

當客戶代碼通過屬性來訪問實值型別成員時,實際返回的是實值型別的副本。對該副本的任何更改都不會影響對象的內部狀態。客戶代碼可以根據自己的需要更改該副本,以達到它們的目的這不會影響內部狀態。

常量類型,如System.String,也是安全的。我們可以在類型中安全地返回string或者其他常量類型,客戶代碼不可能對它們做任何更改,因此可以確保類型內部狀態的安全。

第3種選擇是通過定義介面,將客戶對內部資料成員的訪問限制在一個子集中(參見條款19)。當我們建立類時,可以建立一組介面來支援類型功能的子集。通過使用介面向外界提供類型的功能,我們可以將內部資料遭受無意更改的可能性最小化。客戶代碼可以通過我們提供的介面(不包括類型的全部功能)訪問內部對象。例如,使用IListSource介面向外提供 DataSet的功能就是這種策略的一個應用。某些“詭計多端”的程式員可能會通過猜測實現介面的物件類型,然後使用強制轉型來破壞這種策略。但是這樣的做法肯定會造成一些bug。

最後一種策略:封裝器(wrapper)對象,在System.DataSet類中也有應用。DataViewManager類為我們提供了訪問DataSet的方式,但它卻阻止我們調用那些DataSet類上的變動性方法:

 

public class MyBusinessObject

{

// 唯讀屬性提供了對私人資料成員的訪問:

private DataSet _ds;

public DataView this[ string tableName ]

{

    get

    {

      return _ds.DefaultViewManager.

        CreateDataView( _ds.Tables[ tableName ] );

    }

}

}

// 訪問dataset:

DataView list = bizObj[ "customers" ];

foreach ( DataRowView r in list )

Console.WriteLine( r[ "name" ] );

 

 

DataViewManager 通過建立一些DataView來訪問DataSet中的各個資料表。這樣,使用者就無法更改DataSet中的表了。我們可以對每個DataView進行配置以支援對單個資料元素的更改。但是客戶代碼無法更改其中的表或者資料列。因為讀/寫是被預設支援的,所以客戶代碼仍然可以添加、修改或刪除單個的資料條目。

在探討如何建立一個完全唯讀資料檢視之前,先來看看當允許外部客戶代碼更改資料時,我們有什麼樣的辦法可以響應這種更改?這很重要,因為我們可能經常需要將一個DataView匯出到UI控制項上,以支援使用者編輯資料。大家肯定都用過Windows Forms的資料繫結功能,它可以協助使用者編輯對象的私人資料。DataSet中的DataTable類觸發的事件使其可以很容易地實現Observer (觀察者)模式:我們的類可以響應客戶代碼對其所做的任何改變。當DataSet中的DataTable的任何列或者行發生改變時,都會觸發相關的事件。在將一個編輯動作提交給DataTable之前,會有ColumnChanging和RowChanging事件被觸發。在提交改變之後,會有 ColumnChanged和RowChanged事件被觸發。

當希望將內部資料元素暴露給外界,供外部客戶代碼更改時,也可以利用這種技巧,但我們需要對這些更改進行校正和響應。我們的類可以訂閱那些由內部資料結構產生的事件。然後讓事件處理器通過更新其他內部狀態,來對更改進行校正和響應。

回到原來的問題上,我們希望允許客戶代碼查看資料,但卻不希望它們做任何更改。當我們的資料存放區在一個DataSet中時,我們可以通過建立一個不允許更改的DataView,來確保這一點。 DataView類中包含的屬性允許我們指定是否支援在特定表上的添加、刪除、更改,甚至排序操作。我們可以建立一個索引器來根據所請求的使用索引器的表,返回一個定製的DataView:

 

public class MyBusinessObject

{

// 唯讀屬性提供了對私人資料成員的訪問:

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;

    }

}

}

// 訪問DataSet:

    IList dv = bizOjb[ "customers" ];

    foreach ( DataRowView r in dv )

      Console.WriteLine( r[ "name" ] );

 

 

在上面的類中,我們使用IList介面來返回特定資料表的視圖。我們可以在任何集合上使用IList介面,它並不局限於DataSet。我們不應該簡單地返回 DataView對象,因為使用者可以很容易地再次啟用它的編輯、增加和刪除能力。通過定製返回的視圖,我們則可以避免對鏈表中的對象的更改。返回 IList介面事實上禁止了使用者改變DataView對象的操作許可權。

綜上所述,將參考型別通過公有介面暴露給外界,將使得類型的使用者不用通過我們定義的方法和屬性,就能夠更改對象的內部結構。這違反了我們通常的直覺,會導致常見的錯誤。如果我們匯出的是引用而非值,那就需要改變類型的介面。如果只是簡單地返回內部資料,那麼我們實際上就給外界賦予了訪問內部成員的許可權。客戶代碼可以調用成員中任何可用的方法。通過使用介面或者封裝器對象向外界提供內部的私人資料,我們可以限制外界對它們的訪問能力。當希望客戶代碼更改內部資料元素時,我們應該實現Observer(觀察者)模式,以使對象可以對更改進行校正或響應。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.