這個做法在做法 1 有稍微提到,就是資料封裝(Data encapsulation) 的概念,要避免把私有資料的 reference 回傳,否則就有被修改的風險。

首先看看下面這段程式碼,MyImportantClass 會產生一筆重要的唯讀資料,但是它提供了一個 GetImportantData 方法能夠把這筆資料的參考 傳出去,這樣就能透過 MyClass 光明正大地修改這比重要的資料了。

void Main()
{
	var m = new MyClass();
	Console.WriteLine(m.GetData());
	m.Data.Clear();
	Console.WriteLine(m.Data);
}

public class MyClass
{
	public List<int> Data { get; set; }
	public MyClass()
	{
		Data = MyImportantClass.GetImportantData();
	}

	public List<int> GetData()
	{
		return MyImportantClass.GetImportantData();
	}

	private static class MyImportantClass
	{
		private static List<int> ImportantData { get; }

		static MyImportantClass()
		{
			ImportantData = Enumerable.Range(1, 10).ToList();
		}

		internal static List<int> GetImportantData()
		{
			return ImportantData;
		}
	}
}

這個操作破壞了封裝,因為呼叫者能略過你的物件去修改封裝在內部的參考,這會導致沒有經驗的工程師誤用你的 API,或者會有工程師在這個漏洞之上 安插惡意的程式碼,現在有四種方式可以修正這個問題並保護你內部的資料安全。

  1. value types
  2. immutable types
  3. interfaces
  4. wrappers

我們知道 value types 存取屬性時都是用複製的,所以可以透過這個特性確保回傳的值是一份新複製的版本,這樣呼叫者怎麼修改都不會影響到原始狀態。

確保回傳 immutable types 也是安全的,因為呼叫者沒有權力修改內部狀態。

第三個方式是透過自訂介面提供一組可操作內部狀態的方法,透過這種方式最小化內部狀態被修改的可能性, 這樣呼叫者就只能依靠這組方法操作而不是把物件的完整修改權力都公開出去。

最後一種方式是提供一個 wrappers 物件,也就是在原有物件上在包上一層 ReadOnly 的物件,直接達到避免被修改的目的。 例如在 List<T> 外面套上一層 ReadOnlyCollection<T>,因為 ReadOnlyCollection 本身就沒有提供修改的方法所以可以達到避免被修改的需求。

void Main()
{
	var m = new MyClass();
	Console.WriteLine(m.GetData());
	Console.WriteLine(m.GetData().GetHashCode());
	//m.Data.Clear();
}

public struct MyClass
{
	public ReadOnlyCollection<int> Data { get; set; }
	public MyClass()
	{
		Data = new ReadOnlyCollection<int>(MyImportantClass.GetImportantData());
	}

	public ReadOnlyCollection<int> GetData()
	{
		return new ReadOnlyCollection<int>(MyImportantClass.GetImportantData());
	}

	private struct MyImportantClass
	{
		private static List<int> ImportantData { get; }

		static MyImportantClass()
		{
			ImportantData = Enumerable.Range(1, 10).ToList();
		}

		internal static List<int> GetImportantData()
		{
			return ImportantData;
		}
	}
}

Summary

這個做法在討論那些可以略過你的設計直接修改內部狀態的問題,所以在設計方法時要額外注意回傳的是 reference 還是 value, 另外提供了幾個設計方式避免這個問題,可以直接用 wrappers 套上一個唯讀層是最方便的。