2010-10-29 45 views
6

檢查對SO的earlier question,我開始思考其中類公開的值,如集合,由值的類型實現的接口的情況。在下面的代碼示例中,我使用了一個List,並將該列表公開爲IEnumerable。從接口投放到基礎類型

通過IEnumerable接口顯露列表定義該列表僅列舉以上,未改性的意圖。但是,由於實例可以重新轉換回列表,因此列表本身當然可以修改。

我還包括在樣品中的一個版本,通過各方法被調用時複製列表項的引用到一個新的列表,從而防止改變底層列表可防止變形例的方法的。

所以我的問題是,應該所有的代碼暴露的具體類型實現接口的複製操作的方式做到這一點?會有一個語言結構,明確表示「我想通過一個接口來揭露這個值,並調用代碼應該只能夠通過接口來使用這個值」的價值?其他人使用什麼技術來防止在通過接口公開具體值時出現這些意外副作用。

請注意,我明白,所示的行爲是預期的行爲。我並不是聲稱這種行爲是錯誤的,只是它確實允許使用除表達的意圖以外的功能。也許我爲界面分配了太多的意義 - 把它看作一個功能約束。思考?

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace TypeCastTest 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      // Demonstrate casting situation 
      Automobile castAuto = new Automobile(); 

      List<string> doorNamesCast = (List<string>)castAuto.GetDoorNamesUsingCast(); 

      doorNamesCast.Add("Spare Tire"); 

      // Would prefer this prints 4 names, 
      // actually prints 5 because IEnumerable<string> 
      // was cast back to List<string>, exposing the 
      // Add method of the underlying List object 
      // Since the list was cast to IEnumerable before being 
      // returned, the expressed intent is that calling code 
      // should only be able to enumerate over the collection, 
      // not modify it. 
      foreach (string doorName in castAuto.GetDoorNamesUsingCast()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.WriteLine(); 

      // -------------------------------------- 

      // Demonstrate casting defense 
      Automobile copyAuto = new Automobile(); 

      List<string> doorNamesCopy = (List<string>)copyAuto.GetDoorNamesUsingCopy(); 

      doorNamesCopy.Add("Spare Tire"); 

      // This returns only 4 names, 
      // because the IEnumerable<string> that is 
      // returned is from a copied List<string>, so 
      // calling the Add method of the List object does 
      // not modify the underlying collection 
      foreach (string doorName in copyAuto.GetDoorNamesUsingCopy()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.ReadLine(); 
     } 
    } 

    public class Automobile 
    { 
     private List<string> doors = new List<string>(); 
     public Automobile() 
     { 
      doors.Add("Driver Front"); 
      doors.Add("Passenger Front"); 
      doors.Add("Driver Rear"); 
      doors.Add("Passenger Rear"); 
     } 

     public IEnumerable<string> GetDoorNamesUsingCopy() 
     { 
      return new List<string>(doors).AsEnumerable<string>(); 
     } 
     public IEnumerable<string> GetDoorNamesUsingCast() 
     { 
      return doors.AsEnumerable<string>(); 
     } 
    } 

} 
+6

你可以使用反射來打破其他規則並訪問私有成員。國際海事組織,你已經宣佈它是一個'IEnumerable';如果有人想「欺騙」並將其轉換爲您未定義爲公共API的一部分,那麼這就是他們的業務 - 以及風險。 – 2010-10-29 21:27:10

+0

@Kirk Woll - 將反射作爲「違反規則」的一種手段,這是一個很好的觀點。作爲這個問題的一部分,我確實希望其他人關於是否將界面轉換爲具體類型甚至構成了「違反規則」的形式,或者如果大多數人認爲這是API設計中應該考慮的預期選項,那麼他們的反饋意見。感謝您的評論。 – JeremyDWill 2010-10-29 21:36:03

回答

5

您可以通過使用AsReadOnly()來防止這種惡意行爲。但我認爲真正的答案是,除了暴露的接口/合約之外,不應該依賴返回類型等其他任何東西。做其他任何事情都會影響封裝,阻止您將實現交換給其他不需要的實現使用列表,而是隻是一個T [],等等,等等

編輯:

和下鑄像你提到的基本上是違反了Liskov Substition Principle的,讓所有的技術和材料。

+0

除非API是圍繞使客戶端轉換返回的對象(因爲它們只傳遞'object'類型),否則客戶端應該*不要*將接口轉換爲具體類型。在IEnumerable的情況下,代碼應該類似於'var doorNames = new List (castAuto.GetDoorNamesUsingCast());' – 2010-10-29 21:42:01

2

在這樣的情況下,你可以定義它實現IEnumerable<T>自己的集合類。在內部,你的收藏可以保持一個List<T>,然後你可以只返回底層列表的枚舉:

public class MyList : IEnumerable<string> 
{ 
    private List<string> internalList; 

    // ... 

    IEnumerator<string> IEnumerable<string>.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 
} 
+0

這確實解決了示例中的特定問題,並更清楚地傳達了此場景中的使用意圖。 – JeremyDWill 2010-10-29 21:41:43

1

有一件事情我已經學會使用.NET的工作(並與一些人誰是快速跳轉到一個黑客的解決方案)是,如果不出意外,反射會經常讓人們路過你的「保護」。

接口不是鐵打的編程的束縛,他們是一個承諾,你的代碼對任何其他代碼說:「我可以肯定地做這些事情。」如果「欺騙」和鑄鐵接口對象到一些其他的對象,因爲你,程序員,知道程序沒有,那麼你就打破了合同的東西。其結果是較差的可維護性,並且沒有人會搞亂任何東西該執行鏈,以免其他對象送送下來,不正確地蒙上了依賴。

喜歡做的事情只讀或隱藏實際列表的包裝落後於其他招數也只能停差距。如果你真的想要的話,你可以使用反射來輕鬆挖掘私人列表。我認爲你可以應用某些屬性來防止人們反思。

同樣,只讀列表不是真的。我大概可以找出一種方法來修改列表本身。我幾乎可以肯定地修改列表中的項目。所以readonly是不夠的,也不是一個副本或一個數組。您需要對原始列表進行深層複製(克隆)以在某種程度上實際保護數據。

但是真正的問題是,你爲什麼如此拼命對抗合同你寫了。有時反射黑客是一個方便的解決方法,因爲當別人的圖書館設計得不好時,並沒有公開它需要的東西(或者一個錯誤需要你去挖掘它來修復它)。但是,當你控制界面和消費者界面,沒有理由不讓公開暴露的界面像你需要它完成工作一樣強大。

或者簡稱:如果您需要一個列表,請不要返回IEnumerable,返回一個List。如果你有一個IEnumerable,但你實際上需要一個列表,那麼從IEnum創建一個新列表並使用它更安全。有很少的原因(甚至更少,也許沒有,很好的理由),因爲「我知道它實際上是一個列表,所以這將起作用」。

是的,你可以採取措施試圖阻止人們這樣做,但1)你堅持打破系統的人越難對抗,他們就越難打破它,2)他們只看着爲更多的繩索,最終他們會得到足夠的自己吊死。

2

一個接口是實現它必須做的最小集合的約束(即使「做」不過是投擲NotSupportedException;或者甚至是NotImplementedException)。這不是一個限制,要麼阻止執行更多,要麼阻止調用代碼。