2014-09-03 15 views
6

W przestrzeni nazw dostępna jest poręczna klasa statyczna Assert do obsługi twierdzeń zachodzących w testach.Microsoft.VisualStudio.TestTools.UnitTesting.Assert metoda ogólna przeciąża zachowanie

Coś, co mnie zasmuciło, polega na tym, że większość metod jest wyjątkowo przeciążona, a na dodatek ma ogólną wersję. Specyficznym przykładem jest Assert.AreEqual który ma 18 przeciążeń, a wśród nich:

Assert.AreEqual<T>(T t1, T t2) 

Co jest stosowanie tej metody rodzajowe? Początkowo myślałem, że to sposób na bezpośrednie wywołanie metody IEquatable<T> Equals(T t), ale tak nie jest; zawsze będzie wywoływać nietypową wersję object.Equals(object other). Dowiedziałem się na własnej skórze po kodowaniu kilku testów jednostkowych spodziewających się takiego zachowania (zamiast wcześniejszego sprawdzania definicji klasy Assert, tak jak powinienem).

W celu wywołania generyczną wersję Equals metoda rodzajowa będzie musiał być zdefiniowany jako:

Assert.AreEqual<T>(T t1, T t2) where T: IEquatable<T> 

Czy istnieje dobry powód, dlaczego to nie zostało zrobione w ten sposób?

Tak, tracisz sposób ogólny dla wszystkich tych typów, które nie wdrażają IEquatable<T>, ale nie jest to wielka strata i tak jak równość będzie sprawdzane przez object.Equals(object other), więc Assert.AreEqual(object o1, object o2) jest już wystarczająco dobry.

Czy obecna metoda generyczna oferuje zalety, których nie rozważam, czy jest tak, że nikt nie zastanawiał się nad tym, ponieważ to nie jest tak dużo? Jedyną korzyścią, jaką widzę, jest bezpieczeństwo typu argumentacyjnego, ale wydaje się ono słabe.

Edit: Naprawiono błąd, gdzie trzymane odnosząc się do IComparable kiedy oznaczało IEquatable.

+0

Nie można tego ograniczyć, ponieważ dowolne T bez własnego przeciążenia naturalnie rozwiązałoby tę metodę jako część rozdzielczości przeciążania i tylko wtedy kompilator narzekałby na niespełnione ograniczenie dla dowolnego T, które nie implementuje interfejsu. Co do niepoprawnego założenia, czy sprawdziłeś dokumentację metody przed zbudowaniem swojej strategii testowej? –

+0

AnthonyPegram: jeśli ograniczasz to, nie możesz przekazać żadnego T, które nie implementuje interfejsu, więc jak dokładnie przeciążenie nie powiodło się w środowisku wykonawczym? – InBetween

+1

To by się nie udało w czasie kompilacji, ale rozdzielczość przeciążenia już zdecydowałaby o przeciążeniu T i * następnie * sprawdzenie ograniczenia (i niepowodzenie). Ograniczenia nie są częścią podpisu. Jeśli przeszukujesz tę frazę, znajdziesz mnóstwo zasobów (tutaj i gdzie indziej, w szczególności bloga Erica Lipperta), które podadzą ci więcej szczegółów. –

Odpowiedz

4

Metoda z tym ograniczeniem byłaby nieidealna z powodu często spotykanego problemu z constraints not being part of the signature.

Problem polega na tym, że dla każdego T, który nie jest objęty własnym specyficznym przeciążeniem, kompilator wybrałby ogólną metodę AreEqual<T> jako najlepsze dopasowanie podczas przeciążania, tak jak w rzeczywistości przez dokładne dopasowanie. W innym etapie procesu kompilator oceni, że T przekazuje ograniczenie. Dla każdego T, które nie implementuje IEquatable<T>, ta kontrola zakończy się niepowodzeniem i kod nie zostanie skompilowany.

Rozważmy następujący uproszczony przykład kodu biblioteki testów jednostkowych i klasę, która może istnieć w bibliotece:

public static class Assert 
{ 
    public static void AreEqual(object expected, object actual) { } 
    public static void AreEqual<T>(T expected, T actual) where T : IEquatable<T> { } 
} 

class Bar { } 

Klasa Bar nie implementuje interfejs w ograniczeń. Gdybyśmy byli następnie dodać następujący kod do testów jednostkowych

Assert.AreEqual(new Bar(), new Bar()); 

Kod zawiedzie skompilować z powodu niezaspokojonego przymusu w sprawie sposobu, który jest najlepszym kandydatem. (Bar substytuty T, co sprawia, że ​​lepszym kandydatem niż object.)

typu „Bar” nie mogą być stosowane jako parametr typu „T” w ogólnym typie lub metodzie „Assert.AreEqual <T> (T, T) ". Nie ma żadnej niejawnej konwersji odniesienia z "Bar" do "System.IEquatable <Bar>".

W celu zaspokojenia kompilator i umożliwić nasza jednostka kod test, aby skompilować i uruchomić, musielibyśmy oddać co najmniej jedno wejście do sposobu object tak że przeciążenie nierodzajową może być wybrany, i to byłoby prawdą, że pod warunkiem, że T może istnieć w twoim własnym kodzie lub kodzie, który chcesz wykorzystać w twoich testowych przypadkach, które nie implementują interfejsu.

Assert.AreEqual((object)new Bar(), new Bar()); 

Należy więc zadać pytanie - czy byłoby to idealne? Jeśli pisałeś bibliotekę testującą jednostki, czy utworzyłbyś taką metodę z tak nieprzyjaznym ograniczeniem? Podejrzewam, że nie, a implementatorzy biblioteki testowej jednostki Microsoft (bez względu na to, czy nie) również nie.

+0

+1 Dzięki za sprawienie, że zrozumiem, co się dzieje. Zastanawia mnie jednak, dlaczego ograniczenia typu nie są częścią rozdzielczości przeciążenia ... Zastanawia mnie również, dlaczego kompilator zgłasza błąd zamiast wydawania ostrzeżenia i cofania się do 'AreEqual (obiekt oczekiwany ...'. –

1

Zasadniczo, ogólna siła przeciążania umożliwia porównanie dwóch obiektów tego samego typu. Jeśli zmienisz typ oczekiwanej lub faktycznej wartości, pojawi się błąd kompilacji. Oto MSDN blog opisujące to całkiem dobrze.

+0

Tak, o tym wspominam w pytaniu ... jedyną zaletą jest bezpieczeństwo typu argumentowego, ale w tym konkretnym przypadku jest to raczej złe. – InBetween

1

można dekompilować metodę i zobaczyć, że wszystko to naprawdę nie jest dodać czek typu (poprzez ILSpy), która nie jest jeszcze zrobione poprawnie moim zdaniem (sprawdza typy po równości):

public static void AreEqual<T>(T expected, T actual, string message, params object[] parameters) 
{ 
    message = Assert.CreateCompleteMessage(message, parameters); 
    if (!object.Equals(expected, actual)) 
    { 
     string message2; 
     if (actual != null && expected != null && !actual.GetType().Equals(expected.GetType())) 
     { 
      message2 = FrameworkMessages.AreEqualDifferentTypesFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), expected.GetType().FullName, Assert.ReplaceNulls(actual), actual.GetType().FullName); 
     } 
     else 
     { 
      message2 = FrameworkMessages.AreEqualFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), Assert.ReplaceNulls(actual)); 
     } 
     Assert.HandleFailure("Assert.AreEqual", message2); 
    } 
} 

Teoretycznie może użyć EqualityComparer<T>.Default, aby użyć generycznych równań, jeśli są dostępne, ale w przeciwnym razie byłby inny niż ogólny. To nie wymagałoby wtedy ograniczenia do IEquatable<T>. Odmienne zachowanie dla ogólnych i nietypowych metod równań to zapach kodowy, IMO.

Szczerze mówiąc, ogólne przeciążenie nie jest niezwykle użyteczne, chyba że wypiszesz parametr ogólny. Nie mogę policzyć ile razy błędnie wpisałem właściwość lub porównałem dwie właściwości, które są różnymi typami i wybrały przeciążenie AreEqual(object,object). Daje mi to dużo później awarii w czasie wykonywania niż kompilacji.