html | print_background | ||||||||
---|---|---|---|---|---|---|---|---|---|
|
false |
Krok ten będzie miał niewiele zadań do wykonania (literalnie), ale jest prawdopodobnie najtrudniejszy.
Określisz w nim pojęcie "takie same", czyli kiedy dwa ułamki są sobie równe (implementacja IEquatable
, IEquatable<BigRational>
, generowanie hashkodu, przeciążenie operatorów ==
oraz !=
), utworzysz testy jednostkowe.
Wykonuj zadania w podanej kolejności. Wykorzystaj technikę TDD (Test Driven Development) - najpier testy, potem kod.
-
W projekcie typu Class library dodaj nową klasę. Plik nazwij
BigRationalEquals.cs
. -
Zmień nazwę klasy na
struct BigRational
(formalnie: powtórz nagłówek strukturyBigRational
). Dodaj słowo kluczowepartial
przedstruct
. Korzystasz z funkcjonalności dzielenia kodu klasy lub struktury na wiele plikówpartial class
. Rozbudowę strukturyBigRational
w zakresie tego kroku przeprowadzisz w tym pliku. W przypadku dzielenia kodu na wiele plików słowo kluczowepartial
musi być powtórzone w każdym nagłówku klasy/struktury w każdym pliku. W plikuBigRational.cs
możesz dodaćglobal using System.Numerics;
(wprowadzone w C#10) - dzięki temu w całym projekcie biblioteki i we wszystkich jego plikach z kodem ta przestrzeń nazw będzie dostępna. -
W projekcie z testami jednostkowymi utwórz plik o nazwie
BigRationalEqualsUnitTests.cs
typu class. Możesz to wykonać kolejno poleceniami: Add > Class..., a następnie - podglądając pierwszy plik z testami jednostkowymi - uzupełnij kod klasy testującejBigRationalEqualsUnitTests
tak, aby całe solution poprawnie się kompilowało. Testy jednostkowe związane z implementacją równości ułamków zapisz w tym pliku. -
Opracuj odpowiednie przesłonięcie metody
Equals(Object)
. Równocześnie MUSISZ przesłonićGetHashCode()
- w C#10 wykorzystaj klasęHashCode
i metodę Combine()`. -
Zaimplementuj interfejs
IEquatable<BigRational>
(wymaga implementacji strongly typedEquals<T>(T other)
). -
Zaimplementuj statyczną wersję metody
Equals
o sygnaturze:public static bool Equals(BigRational u1, BigRational u2)
-
Zaimplementuj przeciążenie operatora
==
. Będziesz MUSIAŁ równocześnie zaimplementować przeciążenie operatora!=
. -
Wszystkie powyższe metody muszą być wzajemnie spójne!
-
Opracuj testy jednostkowe.
-
Zwróć uwagę na sytuacje specjalne (
NaN
,+Infinity
,-Infinity
). W C# w typie zmiennoprzecinkowym jakakolwiek relacja zNaN
daje w efekciefalse
!
-
Poczytaj o przesłanianiu
Equals
w C#: -
Poczytaj o implementowaniu interfejsu
IEquatable<T>
, a w szczególności o metodzieIEquatable<T>.Equals(T)
. -
Równość jest relacją równoważności (ściśle zdefiniowane pojęcie matematyczne), spełnia zatem następujące warunki:
a. zwrotność:
x.Equals(x)
zwracatrue
b. symetria:
x.Equals(y)
zwraca tę samą wartość, coy.Equals(x)
c. przechodniość: jeżeli
x.Equals(y)
zwracatrue
orazy.Equals(z))
zwracatrue
, wtedyx.Equals(z)
zwracatrue
Wymagania dotyczące
Equals
uzupełnione są o kolejne warunki:d. dla niepustych (nie-
null
) referencjix
orazy
, wielokrotne wywołaniex.Equals(y)
konsekwentnie zwracatrue
lub konsekwentnie zwracafalse
, o ile żadna informacja związana zx
orazy
nie uległy zmianie (nielosowość działania, niezależność od stanu aplikacji)e.
x.Equals(null)
zwracafalse
f.
x.Equals(y)
zwracafalse
, gdyy != false
, jesli typx
oraz typy
są nieporównywalneg.
x.Equals(y)
zwracatrue
, gdyy != false
orazx
iy
są tego samego typu i są semantycznie tożsameh. Strongly typed
Equals<T>(T other)
musi działać tak samo jakEquals(object)
i.
x.Equals(y)
zwracatrue
jesli obax
orazy
sąNaN
(dla typów liczbowych)j.
x.Equals(y)
zwracatrue
jesli obax
orazy
są tego samego rodzaju nieskończonością (dla typów liczbowych)k.
GetHashCode
powinien zwracać tę samą wartość, gdy jest wywołany dla obiektów tożsamych (smantycznie równych). UWAGA: nie ma wymogu działania przeciwnego, tzn. aby zwracane wartości były różne, gdy dwa obiekty nie są równe.l. Metoda statyczna
Equals(x, y)
oraz operator==
są sobie równoważne * dla obu argumentównull
zwracatrue
(w C#null == null
) * zwracafalse
, jeśli jeden z argumentów jestnull
a drugi nie-null
* dla obu argumentów nie-null
zwraca taki sam wynik, co metoda instancjiEquals
m. Operator
!=
zwraca przeciwne wyniki niż operator==
n.
Equals
nie zgłasza żadnych wyjątkówPodane warunki przydadzą Ci się do opracowania testów jednostkowych Twojej implementacji równości ułamków.
🛈: Dwa ułamki są sobie równe, jeżeli należą do tej samej klasy równoważności względem relacji równości (też pojęcie stricte matematyczne). Oznacza to, że np. 2/4 == 1/2. Ale tym problemem nie musimy się przejmować, ponieważ nasza implementacja przewiduje upraszczanie ułamków. Stąd, porównywanie ułamków może odbyć się pole po polu, jak domyślnie dla typów strukturalnych.
-
Prawa logiki przydają się przy budowaniu testów jednostkowych. Przykładowo, dla testu przechodniości relacji
Equals
:jeżeli
x.Equals(y)
zwracatrue
orazy.Equals(z))
zwracatrue
, wtedyx.Equals(z)
zwracatrue
W logice, zdanie ( (p ⇒ q) ⇔ (¬p ∨ q) ) jest tautologią - czyli zawsze prawdziwe. Zatem, zamiast używać implikacji, możemy ją zastapić odpowiednią alternatywą.
W naszym przypadku, zdanie
(a ∧ b ⇒ c)
, gdziea
oznaczax.Equals(y)
,b
oznaczay.Equals(z)
, zaśc
oznaczax.Equals(z)
zapiszemy w równoważny sposób, bez użycia implikacji:¬(a ∧ b) ∨ c
.Korzystając z prawa de Morgana, przekształcimy je w
¬a ∨ ¬b ∨ c
.Pozostaje w teście sprawdzić, czy dla dowolnych danych testowych zdanie to jest zawsze prawdziwe. Jeśli nie - to powodem bedzie wadliwa implementacja
Equals
.[DataTestMethod] [DataRow(1, 2, 1, 2, 1, 2)] [DataRow(1, 2, 2, 4, 3, 6)] [DataRow(-1, 2, 2, -4, -3, 6)] [DataRow(1, 2, -1, 2, 1, 2)] [DataRow(1, 2, 1, 3, 1, 3)] [DataRow(1, 2, 1, 2, 1, 3)] public void Equals_Przechodniosc_ZPrawLogiki_DowolneDane(int u1l, int u1m, int u2l, int u2m, int u3l, int u3m) { BigRational x = new (u1l, u1m); BigRational y = new (u2l, u2m); BigRational z = new (u3l, u3m); Assert.IsTrue( !x.Equals(y) || !y.Equals(z) || x.Equals(z) ); }
-
Przeanalizuj i ustal różnice między trzema sposobami porównywania:
object.ReferenceEquals(object objA, object objB)
objA.Equals(objB)
objA == objB
-
Co to jest
NullReferenceException
. Kiedy się może pojawić? -
Przeanalizuj porównywanie do
null
(w odniesieniu do typów referencyjnych):if( x == null ) ...
if( x is null ) ...
if( x.Equals(null) ) ...
if( object.ReferenceEquals( x, null ) ...
Zwróć uwagę na ukryte niebezpieczeństwa. W naszym przypadku projektowany typ jest wartościowy i niedopuszcza wartości
null
, zatem powyższe sytuacje są hipotetyczne. -
Porównywać możesz obiekty tego samego typu (czasami dopuszczalne dla podtypu). Jak sprawdzić, czy dwa obiekty są tego samego typu?
Przeanalizuj poniższy kod:
//jakie są różnice (czy są) if (!(obj is Ulamek)) return false; if (obj.GetType() != typeof(Ulamek)) return false; if (this.GetType() != obj.GetType()) return false; if (!object.ReferenceEquals(this.GetType(), obj.GetType())) return false;
-
Aby wzbogacić funkcjonalnosc typu, powinniśmy również przewidzieć porównywanie ułamków do liczb innych typów (całkowitych czy zmiennoprzecinkowych). Całkowicie uzasadnionym jest np.
BigRational(1,2) == 0.5; BigRational(3,1) == 3; 5L == BigRational(5,1); BigRational(1/0) == 1.0/0.0; ...
Teoretycznie można to zrobić już teraz, wprowadzając przeciążone implementacje dla
Equals
oraz dla==
, jednak po wdrożeniu mechanizmów konwersji jawnej i niejawnej otrzymamy tę samą funkcjonalność (a nawet większą) - wtedy należałoby refaktoryzować opracowany kod lub nawet go usuwać. -
Dodatkowo (częste pytania egzaminacyjne):
- jaka jest różnica między operatorem
is
orazas
- co zwraca operator
typeof
, a co metodaGetType()
- czym jest
Type
- jaka jest różnica między operatorem