Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Kwestia z mistrzem: https://github.com/dotnet/csharplang/issues/287
Streszczenie
Zezwól deweloperom na uchwytywanie wyrażeń przekazywanych do metody, aby zapewnić lepsze komunikaty o błędach w diagnostycznych/testowych interfejsach API i zmniejszyć liczbę naciśnięć klawiszy.
Motywacja
Gdy weryfikacja asercji lub argumentu zakończy się niepowodzeniem, deweloper chce wiedzieć jak najwięcej o tym, gdzie i dlaczego zakończyła się niepowodzeniem. Jednak dzisiejsze interfejsy API diagnostyczne nie w pełni to ułatwiają. Rozważmy następującą metodę:
T Single<T>(this T[] array)
{
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);
return array[0];
}
Jeśli jedno z asercji nie powiedzie się, tylko nazwa pliku, numer wiersza i nazwa metody zostaną podane w stosie wywołań. Deweloper nie będzie mógł stwierdzić, która asercja nie powiodła się z tych informacji — będzie musiał otworzyć plik i przejść do wskazanego numeru wiersza, aby sprawdzić, co poszło nie tak.
Jest to również przyczyna, dla których struktury testowania muszą zapewnić różne metody asercjowania. W przypadku interfejsu xUnit Assert.True i Assert.False nie są często używane, ponieważ nie zapewniają wystarczającego kontekstu co się nie powiodło.
Chociaż sytuacja jest nieco lepsza w przypadku weryfikacji argumentów, ponieważ nazwy nieprawidłowych argumentów są wyświetlane deweloperowi, deweloper musi ręcznie przekazać te nazwy do wyjątków. Jeśli powyższy przykład został przepisany, aby użyć tradycyjnej weryfikacji argumentu zamiast Debug.Assert, wyglądałoby to następująco:
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];
}
Należy zauważyć, że każdemu wyjątkowi trzeba przekazać nameof(array), mimo że z kontekstu jest już jasne, który argument jest nieprawidłowy.
Szczegółowy projekt
W powyższych przykładach, umieszczenie ciągu "array != null" lub "array.Length == 1" w komunikacie asercji pomoże deweloperowi określić, co się nie powiodło. Wprowadź CallerArgumentExpression: jest to atrybut, za pomocą których platforma może uzyskać ciąg skojarzony z określonym argumentem metody. Dodalibyśmy go do Debug.Assert w następujący sposób
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}
Kod źródłowy w powyższym przykładzie pozostanie taki sam. Jednak kod, który faktycznie generuje kompilator, odpowiadałby
T Single<T>(this T[] array)
{
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
return array[0];
}
Kompilator rozpoznaje specjalnie atrybut w Debug.Assert. Przekazuje ciąg skojarzony z argumentem, o którym mowa w konstruktorze atrybutu (w tym przypadku condition), w miejscu wywołania. Gdy jedna z asercji zakończy się niepowodzeniem, deweloper zobaczy warunek, który okazał się fałszywy, i będzie wiedział, która z nich zawiodła.
W przypadku weryfikacji argumentów atrybut nie może być używany bezpośrednio, ale można go użyć za pomocą klasy pomocniczej:
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];
}
Propozycja dodania takiej klasy pomocniczej do frameworku jest w toku w https://github.com/dotnet/corefx/issues/17068. Jeśli ta funkcja języka została zaimplementowana, propozycję można zaktualizować, aby skorzystać z tej funkcji.
Metody rozszerzeń
Parametr this w metodzie rozszerzenia może być przywołyny przez CallerArgumentExpression. Na przykład:
public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}
contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"
thisExpression otrzyma wyrażenie odpowiadające obiektowi przed kropką. Jeśli jest wywoływana przy użyciu składni metody statycznej, np. Ext.ShouldBe(contestant.Points, 1337), będzie ona zachowywać się tak, jakby pierwszy parametr nie został oznaczony this.
Zawsze powinno istnieć wyrażenie odpowiadające parametrowi this. Nawet jeśli wystąpienie klasy wywołuje metodę rozszerzenia na sobie, np. this.Single() z wnętrza typu kolekcji, this jest wymagany przez kompilator, więc "this" zostanie przekazany. Jeśli w przyszłości ta reguła zostanie zmieniona, możemy rozważyć przekazanie null lub pustego ciągu znaków.
Dodatkowe szczegóły
- Podobnie jak inne atrybuty
Caller*, takie jakCallerMemberName, ten atrybut może być używany tylko dla parametrów z wartościami domyślnymi. - Dozwolone są wiele parametrów oznaczonych
CallerArgumentExpression, jak pokazano powyżej. - Przestrzeń nazw atrybutu będzie
System.Runtime.CompilerServices. - Jeśli zostanie podana
nulllub ciąg, który nie jest nazwą parametru (np."notAParameterName"), kompilator przekaże pusty ciąg. - Typ parametru, do którego odnosi się
CallerArgumentExpressionAttribute, musi mieć standardową konwersję zstring. Oznacza to, że nie są dozwolone żadne użytkownikowskie konwersje zstring, a w praktyce oznacza to, że typ takiego parametru musi byćstring,objectlub interfejsem implementowanym przezstring.
Wady i niedogodności
Osoby, które wiedzą, jak używać dekompilatorów, będą mogły zobaczyć części kodu źródłowego w miejscach wywołań metod oznaczonych tym atrybutem. Może to być niepożądane/nieoczekiwane w przypadku oprogramowania zamkniętego źródła.
Chociaż nie jest to wada samej funkcji, źródłem obaw może być to, że istnieje interfejs API
Debug.Assertobecnie, który przyjmuje tylkobool. Nawet jeśli przeciążenie przyjmujące komunikat miało drugi parametr oznaczony tym atrybutem i stał się opcjonalny, kompilator nadal wybierze tę bez komunikatu podczas rozwiązywania przeciążenia. W związku z tym przeciążenie komunikatów nie powinno zostać usunięte, aby móc korzystać z tej funkcji, co byłoby zmianą powodującą niezgodność binarną (chociaż nie źródło).
Alternatywy
- Jeśli możliwość wyświetlenia kodu źródłowego w miejscach wywołań metod używających tego atrybutu okaże się problemem, możemy sprawić, że efekty atrybutu będą opcjonalne. Deweloperzy umożliwią tę funkcję za pomocą atrybutu
[assembly: EnableCallerArgumentExpression]dla całego zestawu, który umieszczają wAssemblyInfo.cs.- W przypadku, gdy efekty atrybutu nie są włączone, wywoływanie metod oznaczonych atrybutem nie byłoby błędem, aby umożliwić istniejącym metodom używanie atrybutu i zachowanie zgodności źródła. Jednak atrybut zostanie zignorowany, a metoda zostanie wywołana z dowolną podaną wartością domyślną.
// 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")
- Aby zapobiec występowaniu problemu zgodności binarnej za każdym razem, gdy chcemy dodać nowe informacje o wywołującym do
Debug.Assert, alternatywnym rozwiązaniem byłoby dodanie strukturyCallerInfodo frameworku, która zawiera wszystkie niezbędne informacje o obiekcie wywołującym.
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);
}
}
Pierwotnie zaproponowano to w https://github.com/dotnet/csharplang/issues/87.
Istnieje kilka wad tego podejścia:
Pomimo tego, że funkcja pay-for-play jest przyjazna, umożliwiając określenie potrzebnych właściwości, nadal może to znacznie zaszkodzić, przydzielając tablicę dla wyrażeń/wywoływania
MethodBase.GetCurrentMethodnawet wtedy, gdy asercyjna przechodzi.Ponadto, przekazywanie nowej flagi do atrybutu
CallerInfonie spowoduje niezgodności, aleDebug.Assertnie ma gwarancji, że faktycznie otrzyma ten nowy parametr z miejsc wywołań skompilowanych ze starą wersją metody.
Nierozwiązane pytania
TBD
Spotkania projektowe
N/A
C# feature specifications