說來也算 .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();
        }
    }
}

順便說兩點設計考量:

  1. 亂數物件 Random 採靜態欄位( Field )而非 GetRandomNum() 重新產生,每次 new Random() 會以當下時時間當作亂數種子,若高速連續執行種子可能相同,會兩次取到一樣的結果。
  2. 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);

Post a comment