Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion Issue: https://github.com/dotnet/csharplang/issues/287
Zusammenfassung
Ermöglichen Sie Entwicklern das Erfassen der an eine Methode übergebenen Ausdrücke, um bessere Fehlermeldungen in Diagnose-/Test-APIs zu ermöglichen und Tastaturanschläge zu reduzieren.
Motivation
Wenn eine Assertions- oder Argumentüberprüfung fehlschlägt, möchte der Entwickler so viel wie möglich darüber wissen, wo und warum die Überprüfung fehlgeschlagen ist. Dies wird jedoch von den heutigen Diagnose-APIs nicht vollständig erleichtert. Berücksichtigen Sie die folgende Methode:
T Single<T>(this T[] array)
{
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);
return array[0];
}
Wenn eine der Assertionen fehlschlägt, werden nur der Dateiname, die Zeilennummer und der Methodenname in der Stapelablaufverfolgung bereitgestellt. Der Entwickler wird nicht in der Lage sein, aus diesen Informationen zu erkennen, welche Assertion fehlgeschlagen ist – er muss die Datei öffnen und zur angegebenen Zeilennummer navigieren, um nachzuvollziehen, was schiefgelaufen ist.
Dies ist auch der Grund, warum Testframeworks eine Vielzahl von Assert-Methoden bereitstellen müssen. Bei xUnit werden Assert.True und Assert.False nicht häufig verwendet, da sie nicht genügend Kontext zu dem bereitstellen, was fehlgeschlagen ist.
Während die Situation für die Argumentüberprüfung etwas besser ist, da dem Entwickler die Namen ungültiger Argumente angezeigt werden, muss der Entwickler diese Namen manuell an Ausnahmen übergeben. Wenn das obige Beispiel neu geschrieben wurde, um die herkömmliche Argumentüberprüfung anstelle von Debug.Assertzu verwenden, würde es wie folgt aussehen:
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];
}
Beachten Sie, dass nameof(array) an jede Ausnahme übergeben werden muss, obwohl es bereits aus dem Kontext klar ist, welches Argument ungültig ist.
Detailentwurf
In den obigen Beispielen würde es dem Entwickler helfen, die Zeichenfolge "array != null" oder "array.Length == 1" in die Fehlermeldung aufzunehmen, um zu bestimmen, was fehlgeschlagen ist. Geben Sie CallerArgumentExpressionein: Es handelt sich um ein Attribut, das das Framework verwenden kann, um die Zeichenfolge abzurufen, die einem bestimmten Methodenargument zugeordnet ist. Wir würden es so zu Debug.Assert hinzufügen
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}
Der Quellcode im obigen Beispiel würde gleich bleiben. Der Code, den der Compiler tatsächlich ausgibt, entspricht jedoch dem
T Single<T>(this T[] array)
{
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
return array[0];
}
Der Compiler erkennt speziell das Attribut auf Debug.Assert. Er übergibt die Zeichenfolge, die mit dem Argument verbunden ist, auf das im Konstruktor des Attributs verwiesen wird (in diesem Fall condition), an die Aufrufstelle. Wenn eine der Assertionen fehlschlägt, wird dem Entwickler die Bedingung angezeigt, die falsch war, und weiß, welche fehlgeschlagen ist.
Für die Argumentüberprüfung kann das Attribut nicht direkt verwendet werden, kann aber über eine Hilfsklasse verwendet werden:
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];
}
Ein Vorschlag, eine solche Hilfsklasse zum Rahmenwerk hinzuzufügen, ist im Gange bei https://github.com/dotnet/corefx/issues/17068. Wenn dieses Sprachfeature implementiert wurde, könnte der Vorschlag aktualisiert werden, um dieses Feature nutzen zu können.
Erweiterungsmethoden
Auf den parameter this in einer Erweiterungsmethode kann von CallerArgumentExpressionverwiesen werden. Zum Beispiel:
public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}
contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"
thisExpression erhält den Ausdruck, der dem Objekt vor dem Punkt entspricht. Wenn sie mit statischer Methodensyntax aufgerufen wird, z. B. Ext.ShouldBe(contestant.Points, 1337), verhält es sich so, als ob der erste Parameter nicht thismarkiert wurde.
Es sollte immer ein Ausdruck vorhanden sein, der dem this-Parameter entspricht. Selbst wenn eine Instanz einer Klasse eine Erweiterungsmethode auf sich selbst aufruft, z. B. this.Single() aus einer Sammlung heraus, wird das this vom Compiler vorgeschrieben, so dass "this" übergeben wird. Wenn diese Regel in Zukunft geändert wird, können wir erwägen, null oder den leeren String zu verwenden.
Zusätzliche Details
- Wie bei den anderen
Caller*Attributen wieCallerMemberNamekann dieses Attribut nur für Parameter mit Standardwerten verwendet werden. - Mehrere mit
CallerArgumentExpressiongekennzeichnete Parameter sind zulässig, wie oben gezeigt. - Der Namespace des Attributs wird
System.Runtime.CompilerServicessein. - Wenn
nulloder eine Zeichenfolge, die kein Parametername ist (z. B."notAParameterName") angegeben wird, übergibt der Compiler eine leere Zeichenfolge. - Der Typ des Parameters, auf den
CallerArgumentExpressionAttributeangewendet wird, muss über eine Standardkonvertierung vonstringverfügen. Dies bedeutet, dass keine benutzerdefinierten Konvertierungen ausstringzulässig sind, und in der Praxis bedeutet, dass der Typ eines solchen Parametersstring,objectoder eine schnittstelle sein muss, die vonstringimplementiert wird.
Nachteile
Personen, die wissen, wie Dekompilierer verwendet werden, können einen Teil des Quellcodes auf Aufrufwebsites für Methoden sehen, die mit diesem Attribut gekennzeichnet sind. Dies kann für Closed-Source-Software nicht erwünscht/unerwartet sein.
Obwohl dies kein Fehler im Feature selbst ist, besteht die Sorge, dass es heute eine
Debug.AssertAPI gibt, die nur eineboolbenötigt. Selbst wenn der zweite Parameter der Überladung, die eine Nachricht entgegennimmt, mit diesem Attribut gekennzeichnet und optional wäre, würde der Compiler bei der Überladungsauflösung die Überladung ohne Nachricht wählen. Daher müsste die Überladung ohne Nachricht entfernt werden, um die Vorteile dieser Funktion zu nutzen, was eine fehlerhafte Änderung der Binärdatei (wenn auch nicht der Quelle) wäre.
Alternativen
- Wenn das Einsehen von Quellcode an Aufrufstellen für Methoden, die dieses Attribut verwenden, ein Problem darstellt, können wir die Auswirkungen des Attributs auf eine Opt-in-Basis ändern. Entwickler ermöglichen dies über ein assemblyweites
[assembly: EnableCallerArgumentExpression]Attribut, das sie inAssemblyInfo.cseinfügen.- Wenn die Auswirkungen des Attributs nicht aktiviert sind, wäre das Aufrufen von Methoden, die mit dem Attribut gekennzeichnet sind, kein Fehler, um vorhandenen Methoden die Verwendung des Attributs und die Beibehaltung der Quellkompatibilität zu ermöglichen. Das Attribut wird jedoch ignoriert, und die Methode wird mit dem angegebenen Standardwert aufgerufen.
// 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")
- Um zu verhindern, dass das Binärkompatibilitätsproblem jedes Mal auftritt, wenn wir neue Aufruferinformationen zu
Debug.Asserthinzufügen möchten, wäre eine alternative Lösung das Hinzufügen einerCallerInfoStruktur zu dem Framework, die alle erforderlichen Informationen zum Aufrufer enthält.
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);
}
}
Dies wurde ursprünglich bei https://github.com/dotnet/csharplang/issues/87vorgeschlagen.
Dieser Ansatz hat einige Nachteile:
Trotz der Möglichkeit, die benötigten Eigenschaften für die Wiedergabe zuzulassen, könnte dies die Leistung erheblich beeinträchtigen, da ein Array für die Ausdrücke/Aufrufe
MethodBase.GetCurrentMethodzugewiesen wird, selbst wenn die Assert erfolgreich ist.Außerdem ist die Übergabe eines neuen Flags an das
CallerInfo-Attribut zwar keine fehlerhafte Änderung, aberDebug.Asserterhält diesen neuen Parameter nicht garantiert von Aufrufseiten, die gegen eine alte Version der Methode kompiliert wurden.
Ungelöste Fragen
TBD
Design-Besprechungen
N/A
C# feature specifications