.NET 靜態屬性的執行時機與設計
5 |
說來也算 .NET 的笨問題,但我第一時間竟有些迷惑,做個實驗強化信念。
疑問是下圖中的 Prop1、Prop2 是否每次取用都會重新執行,傳回不同結果?
完整程式如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace TestStaticProps
{
public class Foo
{
static Random rand = new Random();
static string GetRandomNum()
{
//REF: https://blog.darkthread.net/blog/kb-net/
StackTrace st = new StackTrace(true);
StackFrame sf = st.GetFrame(1);
MethodBase mb = sf.GetMethod();
var value = rand.Next().ToString("X").PadLeft(8, '0');
Debug.WriteLine($"GetRandomRun {value} for {mb.Name}");
return value;
}
public static string Prop1
{
get
{
return GetRandomNum();
}
}
public static string Prop2 => GetRandomNum();
public static string Prop3 = GetRandomNum();
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Round 1");
Console.WriteLine($"Foo.Prop1 = {Foo.Prop1}");
Console.WriteLine($"Foo.Prop2 = {Foo.Prop2}");
Console.WriteLine($"Foo.Prop3 = {Foo.Prop3}");
Console.WriteLine("Round 2");
Console.WriteLine($"Foo.Prop1 = {Foo.Prop1}");
Console.WriteLine($"Foo.Prop2 = {Foo.Prop2}");
Console.WriteLine($"Foo.Prop3 = {Foo.Prop3}");
Console.ReadLine();
}
}
}
順便說兩點設計考量:
- 亂數物件 Random 採靜態欄位( Field )而非 GetRandomNum() 重新產生,每次 new Random() 會以當下時時間當作亂數種子,若高速連續執行種子可能相同,會兩次取到一樣的結果。
- GetRandomNum() 裡用了一段透過 StackTrace() 追蹤呼叫來源的技巧,以得知何時被誰呼叫。
執行結果如下,靜態欄位( Static Field )只會執行一次,值是固定的;靜態屬性( Static Property )則是每次叫用重新執行產生新值。
另外由 Debug.WriteLine() 軌跡可知,靜態欄位是在一開始靜態建構階段執行:
由此可知,靜態屬性的執行時機跟靜態方法沒有兩樣。如果要避免反覆重新計算,可以改成靜態欄位配初始值(如上例中的 Prop3,但邏輯複雜時不適合)、第一次計算後用內部靜態欄位(術語為 Backing Field)保存,或是在靜態建構式中指定,如以下範例中的 Prop4 與 Prop5:
static string GetComplexCalcResult()
{
StackTrace st = new StackTrace(true);
StackFrame sf = st.GetFrame(1);
MethodBase mb = sf.GetMethod();
var value = rand.Next().ToString("X").PadLeft(8, '0');
Debug.WriteLine($"Complex Calculation {value} for {mb.Name}");
System.Threading.Thread.Sleep(5);
return value;
}
static string _Prop4 = null; //Backing Field
public static string Prop4
{
get
{
if (string.IsNullOrEmpty(_Prop4))
{
_Prop4 = GetComplexCalcResult();
}
return _Prop4;
}
}
public static string Prop5 { get; private set; }
static Foo()
{
Prop5 = GetComplexCalcResult();
}
乍看 OK,但這裡面藏了陷阱 - Prop4 在多執行緒環境可能執行多次,寫一小段 Parallel.For 來驗證:
static void Main(string[] args)
{
var results = new List<string>();
Parallel.For(0, 20, (i) =>
{
if (i % 2 == 0)
results.Add($"Foo.Prop4 ({i:00}) = {Foo.Prop4}");
else
results.Add($"Foo.Prop5 ({i:00}) = {Foo.Prop5}");
});
Console.WriteLine(string.Join("\n", results.OrderBy(o => o).ToArray()));
Console.ReadLine();
}
如圖,在多緒同時呼叫下,Prop4 取值動作發生了五次:
這類問題可以加個 lock 簡單解決:(因 string 不適合當成 lock 標的故另設一個 object prop4Sync,若型別合適亦可直接 lock (_Prop4) )
以上是 .NET 型別靜態屬性設計時的一些考量,尤其不要忽略在多緒執行環境(例如被高流量站台的 ASP.NET Request 呼叫)可能被反覆呼叫的狀況,若重複執行會涉及資源消耗、資源鎖定及效能問題,記得要納入考量。
Experiment of static property execution timing and tips of static property designing.
Comments
# by Will
透過 StachTrace() 追蹤呼叫來源 👇 透過 StackTrace() 追蹤呼叫來源
# by fredli
Prop4可以寫成這樣 public static string Prop4 => LazyInitializer.EnsureInitialized(ref _Prop4, () => GetComplexCalcResult()); 或是更簡略的變成 public static string Prop4 => LazyInitializer.EnsureInitialized(ref _Prop4, GetComplexCalcResult);
# by Jeffrey
to fredli, 感謝分享,又學到新招了。 to Will, 感謝指正,已改正。
# by ShaoYu
靜態屬性就是包裝過的靜態方法吧
# by DarkGuo
多執行緒要加Lock object _dataLock = new object(); public static string Prop4 => LazyInitializer.EnsureInitialized(ref _Prop4, ref _dataLock, GetComplexCalcResult);