這個做法建議不要使用 public field 的寫法而是改用 automatically implemented properties, 或者自己在屬性撰寫 getset 存取子來存取 private field

你可能有看過下面這種寫法,這個類別建立了兩個公開的 field 用來記錄資料。

public class User
{
	public string Name;
	public int Age;
}

使用上可以實例化並且對公開 field 進行存取資料的操作。

void Main()
{
	var u = new User();
	u.Name = "Allen";
	u.Age = 30;
	
	Console.WriteLine(u.Name);
}

但實際上這種寫法是非常不好的,因為它違反了資料封裝(Data encapsulation)的原則,我們必須把 field 隱藏起來避免使用者進行意料外的操作, 導致資料被破壞,例如下面使用者就直接把年齡設定為 -1 這個值明顯不符合要求。

u.Age = -1;

所以就衍生出了 private field 搭配 accessor methods 這種比較好的寫法,原理就是寫一個對使用者公開的方法, 裡面可以建立一些基礎的判斷邏輯,並且對私有欄位進行讀取。

void Main()
{
	var u = new User();
	u.SetName("Allen");
	u.SetAge(-1);
	
	Console.WriteLine(u.GetName());
}

public class User
{
	private string _name;
	private int _age;
	
	public string GetName()
	{
		return _name;
	}
	
	public void SetName(string name)
	{
		_name = name;
	}
	
	public int GetAge()
	{
		return _age;
	}
	
	public void SetAge(int age)
	{
		if (age < 0)
			throw new ArgumentException("The value must be greater than or equal to 0", nameof(age));
		_age = age;
	}
}

不過這個寫法需要使用者改變使用習慣,像是設定名稱的時候要把程式碼改呼叫 SetName 方法,而不是像以前那樣直接使用 field 名稱就能進行存取。 也就是說我們在撰寫程式碼的時候需要額外寫這些專門存取用的方法,另外使用者也必須了解這些方法的名稱與使用方式。

所以就出現屬性這個折中的處理方式,使用屬性可以讓我們省略掉寫存取方法的步驟,又能讓存取的寫法跟之前使用 field 名稱的方式一樣。

void Main()
{
	var u = new User();
	u.Name = "Allen";
	u.Age = -1;

	Console.WriteLine(u.Name);
}

public class User
{
	private string _name;
	private int _age;

	public string Name
	{
		get { return _name; }
		set { _name = value; }
	}

	public int Age
	{
		get { return _age; }
		set
		{
			if (value < 0)
				throw new ArgumentException("The value must be greater than or equal to 0", nameof(Age));

			_age = value;
		}
	}
}

不過大部分的屬性在存取時不需要額外的檢查邏輯,所以可以利用 automatically implemented properties 自動實現 field 的方式少寫許多程式碼, 例如下面這樣直接把 Name 變成自動實現的屬性,這樣這個屬性就會自動產生 backing field 來保存我們的資料,並且屬性可以是 virtual, 這樣衍生類別不想要自動實現,也可以改回上面那樣自行實現的寫法,一般的 field 不可以是 virtual

public class User
{
	private int _age;

	public virtual string Name { get; set; }

	public int Age
	{
		get { return _age; }
		set
		{
			if (value < 0)
				throw new ArgumentException("The value must be greater than or equal to 0", nameof(Age));

			_age = value;
		}
	}
}

也可以在介面中定義屬性,同樣這一點 field 也做不到。

public interface INameValuePair<T>
{
	string Name { get; }
	T Value {get; set;}
}

剛剛提到的屬性被稱為 parameterless properties 代表它們讀取時並不需要輸入參數,這種無參數的屬性經常被使用到,不過還有另一種屬性叫做 parameterful properties 也叫做 indexers,使用起來跟陣列輸入 index 差不多,要注意只能用 this 宣告 indexers

void Main()
{
	var myList = new UserList();
	myList[0] = 1;
	Console.WriteLine(myList[0]);
}

public class UserList
{
	private int[] theValues = new int[10];
	public int this[int index]
	{
		get => theValues[index];
		set => theValues[index] = value;
	}
}

但不限於數值型別,像是這裡就使用 Dictionary 當成 field,這樣就可以在 indexers 裡面的 getset 對傳入的資料進行判斷。

void Main()
{
	var myList = new UserList();
	myList["Allen"] = 30;
	Console.WriteLine(myList["Allen"]);
}

public class UserList
{
	private Dictionary<string, int> _names = new Dictionary<string, int>();
	public int this[string name]
	{
		get => _names[name];
		set => _names[name] = value;
	}
}

雖然屬性與欄位表面上看起來功能是一模一樣的,但是要注意屬性當初設計的目的是存取時要跟 field 的操作相同,這只代表它們語法的目的是相同的, 並不代表屬性是用來保存資料用的,實際上欄位才是用來保存資料的。

也就是說假如你一開始用的是 public field 之後想要改成 auto property,雖然不用修改任何程式碼,但實際上編譯的結果是完全不同的。


Summary

這個做法建議只使用 private field 並警告永遠不要寫 public field,如果想要公開存取 private field 那麼就使用 public propertiesprotected properties 來對外揭露私有資料欄位,還說明了 indexers 的使用方式。