共用方式為


CallerArgumentExpression

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異在相關的 語言設計會議(LDM)記錄中被捕捉到。

您可以在 規格一文中深入瞭解將功能規格採用到 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/287

總結

允許開發人員擷取傳遞至方法的運算式,以便在診斷/測試 API 中提供更好的錯誤訊息,並減少輸入的次數。

動機

當判斷提示或自變數驗證失敗時,開發人員想要盡可能瞭解失敗的位置和原因。 不過,現今的診斷 API 無法完全協助進行這項操作。 請考慮下列方法:

T Single<T>(this T[] array)
{
    Debug.Assert(array != null);
    Debug.Assert(array.Length == 1);

    return array[0];
}

當其中一個斷言失敗時,只會在堆疊追蹤中提供檔名、行號和方法名稱。 開發人員將無法從這項資訊中判斷哪個斷言失敗,他們必須開啟檔案並導航至提供的行號,以查看發生了什麼錯誤。

這也是測試架構必須提供各種判斷提示方法的原因。 使用 xUnit 時,Assert.TrueAssert.False 不會經常使用,因為它們未提供足夠的內容來說明失敗的內容。

雖然情況較適合自變數驗證,因為無效的自變數名稱會向開發人員顯示,但開發人員必須手動將這些名稱傳遞至例外狀況。 如果上述範例重寫為使用傳統自變數驗證,而不是 Debug.Assert,則看起來會像這樣

T Single<T>(this T[] array)
{
    if (array == null)
    {
        throw new ArgumentNullException(nameof(array));
    }

    if (array.Length != 1)
    {
        throw new ArgumentException("Array must contain a single element.", nameof(array));
    }

    return array[0];
}

請注意,nameof(array) 必須傳遞至每個例外狀況,儘管從上下文中已經清楚知道哪個參數無效。

詳細設計

在上述範例中,將字串 "array != null""array.Length == 1" 包含在斷言訊息中,可以幫助開發人員判斷哪裡出了問題。 輸入 CallerArgumentExpression:這是架構可用來取得與特定方法自變數相關聯的字串的屬性。 我們會將其新增至 Debug.Assert,如下所示

public static class Debug
{
    public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}

上述範例中的原始程式碼會維持不變。 不過,編譯程式實際發出的程式代碼會對應至

T Single<T>(this T[] array)
{
    Debug.Assert(array != null, "array != null");
    Debug.Assert(array.Length == 1, "array.Length == 1");

    return array[0];
}

編譯程式會特別辨識 Debug.Assert上的屬性。 它會在呼叫點傳遞與在屬性建構子中引用的參數相關聯的字串(在此案例中為 condition)。 當任一個斷言失敗時,開發人員將看到結果為 false 的條件,並知道哪一個失敗。

針對自變數驗證,屬性無法直接使用,但可以透過協助程序類別來使用:

public static class Verify
{
    public static void Argument(bool condition, string message, [CallerArgumentExpression("condition")] string conditionExpression = null)
    {
        if (!condition) throw new ArgumentException(message: message, paramName: conditionExpression);
    }

    public static void InRange(int argument, int low, int high,
        [CallerArgumentExpression("argument")] string argumentExpression = null,
        [CallerArgumentExpression("low")] string lowExpression = null,
        [CallerArgumentExpression("high")] string highExpression = null)
    {
        if (argument < low)
        {
            throw new ArgumentOutOfRangeException(paramName: argumentExpression,
                message: $"{argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");
        }

        if (argument > high)
        {
            throw new ArgumentOutOfRangeException(paramName: argumentExpression,
                message: $"{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");
        }
    }

    public static void NotNull<T>(T argument, [CallerArgumentExpression("argument")] string argumentExpression = null)
        where T : class
    {
        if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);
    }
}

static T Single<T>(this T[] array)
{
    Verify.NotNull(array); // paramName: "array"
    Verify.Argument(array.Length == 1, "Array must contain a single element."); // paramName: "array.Length == 1"

    return array[0];
}

static T ElementAt<T>(this T[] array, int index)
{
    Verify.NotNull(array); // paramName: "array"
    // paramName: "index"
    // message: "index (-1) cannot be less than 0 (0).", or
    //          "index (6) cannot be greater than array.Length - 1 (5)."
    Verify.InRange(index, 0, array.Length - 1);

    return array[index];
}

將這類協助程式類別新增至架構的建議正在進行中,https://github.com/dotnet/corefx/issues/17068。 如果實作此語言功能,可以更新提案以利用這項功能。

擴充方法

擴充方法中的 this 參數可由 CallerArgumentExpression參考。 例如:

public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}

contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"

thisExpression 將接收點號前物件所對應的表達式。 如果使用靜態方法語法呼叫,例如 Ext.ShouldBe(contestant.Points, 1337),它的行為會如同第一個參數未標示 this一樣。

一律應該有對應至 this 參數的表達式。 即使類別的實例本身會呼叫擴充方法,例如從集合類型內部 this.Single(),編譯程式會強制 this,因此會傳遞 "this"。 如果未來變更此規則,我們可以考慮傳遞 null 或空字串。

額外詳細數據

  • 如同其他 Caller* 屬性,例如 CallerMemberName,此屬性只能用於具有預設值的參數。
  • 允許標記 CallerArgumentExpression 的多個參數,如上所示。
  • 屬性的命名空間識別將為 System.Runtime.CompilerServices
  • 如果提供 null 或不是參數名稱的字串(例如 "notAParameterName"),則編譯程式會傳入空字串。
  • 參數 CallerArgumentExpressionAttribute 所屬的類型必須能從 string進行標準轉換。 這表示不允許來自 string 的使用者定義轉換,實際上表示這類參數的類型必須 stringobject或由 string實作的介面。

缺點

  • 知道如何使用反編譯程式的人可以在呼叫點看到一些標示有此屬性的方法的原始程式碼。 對於閉源軟體來說,這可能是意料之外的。

  • 雖然這不是功能本身的缺陷,但有一個令人擔憂的來源可能是,目前有一個 Debug.Assert API,只接受 bool。 即使具有此屬性的第二個參數被標記為選用,且使得參數變得可選擇,編譯器在多載解析中仍會選擇不包含訊息的多載。 因此,必須移除無訊息重載,才能利用這項功能,這將是二進位層級的破壞性變更(但不影響源代碼)。

替代方案

  • 如果在方法的呼叫點看到使用這個屬性的原始程式碼成為問題,我們可以讓屬性的效果選擇性啟用。 開發人員會透過全組件的 [assembly: EnableCallerArgumentExpression] 屬性啟用它,並將該屬性放置在 AssemblyInfo.cs中。
    • 如果未啟用屬性的效果,則呼叫以 屬性標示的方法不會是錯誤,以允許現有的方法使用 屬性並維護來源相容性。 不過,這個屬性會被忽略,並且會以所提供的預設值來呼叫該方法。
// Assembly1

void Foo(string bar); // V1
void Foo(string bar, string barExpression = "not provided"); // V2
void Foo(string bar, [CallerArgumentExpression("bar")] string barExpression = "not provided"); // V3

// Assembly2

Foo(a); // V1: Compiles to Foo(a), V2, V3: Compiles to Foo(a, "not provided")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")

// Assembly3

[assembly: EnableCallerArgumentExpression]

Foo(a); // V1: Compiles to Foo(a), V2: Compiles to Foo(a, "not provided"), V3: Compiles to Foo(a, "a")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
  • 為了避免每次我們想要將新的呼叫端資訊新增至 Debug.Assert時,發生 二進位相容性問題,替代解決方案就是將 CallerInfo 結構新增至包含呼叫端所有必要資訊的架構。
struct CallerInfo
{
    public string MemberName { get; set; }
    public string TypeName { get; set; }
    public string Namespace { get; set; }
    public string FullTypeName { get; set; }
    public string FilePath { get; set; }
    public int LineNumber { get; set; }
    public int ColumnNumber { get; set; }
    public Type Type { get; set; }
    public MethodBase Method { get; set; }
    public string[] ArgumentExpressions { get; set; }
}

[Flags]
enum CallerInfoOptions
{
    MemberName = 1, TypeName = 2, ...
}

public static class Debug
{
    public static void Assert(bool condition,
        // If a flag is not set here, the corresponding CallerInfo member is not populated by the caller, so it's
        // pay-for-play friendly.
        [CallerInfo(CallerInfoOptions.FilePath | CallerInfoOptions.Method | CallerInfoOptions.ArgumentExpressions)] CallerInfo callerInfo = default(CallerInfo))
    {
        string filePath = callerInfo.FilePath;
        MethodBase method = callerInfo.Method;
        string conditionExpression = callerInfo.ArgumentExpressions[0];
        //...
    }
}

class Bar
{
    void Foo()
    {
        Debug.Assert(false);

        // Translates to:

        var callerInfo = new CallerInfo();
        callerInfo.FilePath = @"C:\Bar.cs";
        callerInfo.Method = MethodBase.GetCurrentMethod();
        callerInfo.ArgumentExpressions = new string[] { "false" };
        Debug.Assert(false, callerInfo);
    }
}

這最初是在 https://github.com/dotnet/csharplang/issues/87提出的。

此方法有幾個缺點:

  • 儘管此系統對於交易/支付十分友好,允許您指定所需的屬性配置,但即使在判斷通過的情況下,仍可能因為為表達式配置陣列/呼叫 MethodBase.GetCurrentMethod 而嚴重影響系統效能。

  • 此外,將新旗標傳遞給 CallerInfo 屬性不會造成重大變更,但 Debug.Assert 不保證能從針對舊版本方法編譯的呼叫端接收到新參數。

未解決的問題

待定

設計會議

N/A