今天踩到的低級錯誤,用以下範例重現。

假設有物件 Foo,建構時傳入字串,透過 List<string> ListDictionary<string, int> Stats 兩個唯讀屬性傳回包含字元清單及出現次數統計:

public class Foo {
    public List<string> List => 
        rawData.ToCharArray().GroupBy(c => c)
            .OrderBy(g => g.Key)
            .Select(g => g.Key.ToString())
            .ToList();
    public Dictionary<string, int> Stats =>
        rawData.ToCharArray().GroupBy(c => c)
            .OrderBy(g => g.Key)
            .ToDictionary(g => g.Key.ToString(), g => g.Count());
    string rawData ;
    public Foo(string data)
    {
        rawData = data;
    }
}

每次讀取重新統計有損效能,但原始情境很單純又,只有一處程式用到,兩個屬性也只會讀一次,簡單寫寫就好。

不過呢,你永遠料想不到別人會怎麼用你的元件(這裡的別人包含未來的自己)。在其他模組借用了這顆元件,看到元件傳回 List<string> 的是 ``Dictionary<string, int>`,依直覺加工後想用在別的地方:

var foo = new Foo("AADBDACABC");
foo.List.Add("E");
foo.Stats.Add("E", 2);
Console.WriteLine(string.Join(", ", 
    foo.Stats.Select(o=> $"{o.Key}:{o.Value}")));
Console.WriteLine(string.Join(", ", foo.List));

結局是:明明用 Add() 做了修改,實際輸出卻什麼都沒變,追了一會兒才猛然想起問題出「List 跟 Stats 是現點現做的」。

先不探究這類別介面設計得有問題,把焦點放在「若規格就是要 List 或 Dictionary,有沒有辦法將其設成唯讀防止呼叫端誤用」?

心血來潮,這回不 Google 求解,改問神奇海螺 Github Copilot 看能否得到答案?

使用 Copilot Chat[1],用 how to prevent user from altering the properties or elements of readonly properties (ex: Stats and List)? 提問[2],由 Copilot 回答我學到 IReadOnlyListIReadOnlyDictionary,而 Dictionary<T,T>List<T> 都支援 AsReadOnly() 傳回 ReadOnlyDictionary<TKey,TValue>ReadOnlyCollection<T>

另一種更快的做法是,選取 List 與 Stats 屬性的程式片段[1],按 Ctrl - I 叫出提示輸入列,請 Copilot 將二者換成唯讀版[2],Copilot 會直接改寫,按下 Accept 即可套用,而套用前已可預覽修改後上方的兩個 Add() 出現紅蚯蚓[3]無法編譯:

注意:IReadOnlyList、IReadOnlyDictionary 只防止對清單跟雜湊表進行增刪或置換,無法防止清單或雜湊元素被修改,如下例:

Example of using IReadOnlyList and IReadOnlyDictionary to prevent modifictaion.


Comments

# by Allex

不讓使用者修改,我倒是比較喜歡回傳 IEnumerable<string> 。

# by PH

黑大, 不好意思 請問 「List 跟 Stats 是現點現做的」 這句話是什麼意思

# by Jeffrey

to Allex, 同意,若可修改介面,調成 IEnumerable<T> 跟 IGrouping<TKey, TValue> 是更佳解。

# by Jeffrey

to PH,現點現做是比喻,像餐廳的某些菜色會等客人點菜才下鍋。List 與 Stats 屬性會每次要讀取時才運算產生,概念相似。

Post a comment