More Effective C# 06.確保屬性運作如資料一般 More Effective C# 06.確保屬性運作如資料一般(Ensure That Properties Behave Like Data)

這個做法說明了屬性跟欄位兩者容易搞混的部分,並且要預期使用者會把屬性當成欄位來使用,所以屬性不能做出太複雜最好和欄位的功能差不多。

在做法 1 與做法 2 了解到屬性實際上扮演了兩個角色,一個是保存資料元素有點像是欄位,另一個則是使用 getset 存取子能夠搭配方法與檢查邏輯, 所以過度使用可能會與使用者預期的屬性會有很大的落差,畢竟有很多使用者只是把屬性當成方便使用的欄位而已,假如你的屬性包含了複雜的方法與檢查邏輯 ,可能就會導致誤用。

以這段迴圈為例,其實就是不斷的在讀取 myArray 的 Length 屬性,如果這個讀取需要耗費非常多的時間,或者是進資料庫取回, 那麼這種設計就會跟使用者預期的相差很大。

for (int index = 0; index < myArray.Length; index++)

所以建議是使用做法 2 提到的 Implicit properties 讓編譯器自動實做 backing field 與輕量的存層,最多就是在存取的時候搭配輕量的檢查邏輯。

public string LastName
{
   set
   {
       if (string.IsNullOrEmpty(value))
           throw new ArgumentException("last name can't be null or blank");
       lastName = value;
   }
}

像是下面段程式碼中的 Distance 屬性會在回傳前進行簡單的運算也算是常用的使用方式。

public class Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public double Distance => Math.Sqrt(X * X + Y * Y);
}

但如果計算 Distance 會造成效能瓶頸,可以使用下面的寫法這樣只會進行首次運算,之後就只會讀取緩存的值。

public class Point
{
	private int xValue;
	public int X
	{
		get => xValue;
		set
		{
			xValue = value;
			distance = default(double?);
		}
	}
	private int yValue;
	public int Y
	{
		get => yValue;
		set
		{
			yValue = value;
			distance = default(double?);
		}
	}
	private double? distance;
	public double Distance
	{
		get
		{
			if (!distance.HasValue)
				distance = Math.Sqrt(X * X + Y * Y);
			return distance.Value;
		}
	}
}

以上都還算是合理的使用範圍,但下面這個屬性內部卻是去資料庫進行存取,這種設計會耗費大量的時間,而且使用者也可能不會有這樣的預期, 也可能在讀取的過程中發生錯誤。

public class MyType
{
   public string ObjectName => RetrieveNameFromRemoteDatabase();
}

如果真的確定要這麼做那至少要搭配對應的實作模式來減少衝擊,例如下面也是使用緩存機制,這樣就只會影響第一次讀取。

public class MyType
{
   private string objectName;
   public string ObjectName =>
       (objectName != null) ?
       objectName : RetrieveNameFromRemoteDatabase();
}

這個模式也可改用 .net 提供的 Lazy<T> 類別,所以可以改寫成下面這樣,但是要先確認這個值是否緩存起來也沒關係。

private Lazy<string> lazyObjectName;
public string ObjectName => lazyObjectName.Value;
public MyType()
{
   lazyObjectName = new Lazy<string>(() => RetrieveNameFromRemoteDatabase());
}

下面這對程式碼會使用 set 存取子將資料保存回資料庫內,這種操作就有可能違反使用者的預期,應該不會有人預期呼叫 set 存取子竟然要花這麼多時間吧, 另外 get 存取子也有可能在運行的期間報錯,這會導致排查變得很困難。

public class MyType
{
	private string objectName;
	public string ObjectName
	{
		get
		{
			if (objectName == null)
				objectName = RetrieveNameFromRemoteDatabase();
			return objectName;
		}
		set
		{
			objectName = value;
			SaveNameToRemoteDatabase(objectName);
		}
	}
}

Summary

這個做法在講解屬性已經給大家帶來很快的既定印象,所以預期存取的過程都應該要相當短才是合理的,所以你的屬性違反了這些預期那應該修改屬性的實做內容, 讓屬性在使用上盡量與讀取欄位一樣快速。