html: embed_local_images: false embed_svg: true offline: false toc: false
W kroku tym zdefiniujesz podstawową funkcjonalność projektowanego typu - zdefiniujesz wewnętrzną reprezentację danych ułamka (pola lub właściwości) zapewniając niezmienniczość tworzonych obiektów, zdefiniujesz konstruktory oraz tekstową reprezentację ułamka, określisz zasady dostępu do składników typu, utworzysz testy jednostkowe.
Wykonuj zadania w podanej kolejności.
Będziesz intensywnie korzystał z typu BigInteger
- zapoznaj się z jego dokumentacją.
-
W projekcie typu Class Library utwórz publiczną strukturę
BigRational
(w plikuBigRational.cs
). -
Zdefiniuj właściwości struktury (
Numerator
- licznik,Denominator
- mianownik) jako wartości typuBigInteger
. -
Zapewnij odpowiedni poziom hermetyzacji (wartości licznika i mianownika są udostępniane publicznie za pomocą getterów).
-
Pamiętaj, aby zapewnić niezmienniczość obiektów typu
BigRational
. -
Dostarcz konstruktory obiektów typu
BigRational
. Rozważ różne sytuacje. Pamiętaj o0
w mianowniku! Opracuj testy jednostkowe weryfikujące poprawność działania konstruktorów oraz gettersów. -
Przyjmij, że tekstową reprezentacją ułamka jest postać:
[znak]<<licznik>>/<<mianownik>>
na przykład
-2/3
lub-7/2
, ale nie2/-3
oraz nie1 1/2
.Opracuj odpowiednie przeciążenie metody
ToString()
.Opracuj testy jednostkowe weryfikujące poprawność reprezentacji tekstowej ułamka.
-
Zapewnij, aby ułamek zapamiętany był w postaci nieskracalnej (licznik i mianownik są względnie pierwsze) i zestandaryzowanej - znak ułamka jest znakiem licznika. Opracuj testy jednostkowe weryfikujące tę funkcjonalność.
-
Niezmienniczość obiektów zapewnisz słowem kluczowym readonly (po to zresztą zostało wprowadzone do języka). Od C# 7.2 można deklarować readonly struct - wymusi ono na tobie określone działania. Właściwości udostępniające licznik i mianownik możesz zdefiniować w C# 10 jako
{get; init
}` (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init). -
Dla uproszczenia zapisu, tam gdzie nie jest to zbyt skomplikowane, wykorzystuj notację lambda.
-
Implementacja konstruktorów -- musisz utworzyć przeciążone konstruktory:
BigRational(BigInteger value) { ... } BigRational(BigInteger numerator, BigInteger denominator) { ... }
oczywiście je łańcuchując w odpowiedni sposób. Niestety, nie będziesz mógł skorzystać z mechanizmu parametrów opcjonalnych, ponieważ parametrami sa instancje
BigInteger
. -
⚠️ Problemy z konstruktorem domyślnym (bezargumentowym) i wartością domyślną (default
). PonieważBigRational
jest strukturą, konstruktor bezargumentowy tworzony jest automatycznie. Od C#9 z poprawkami w C#10 możesz go przesłonić (we wcześniejszych wersjach zgłaszany był błąd kompilacji) - na przykład możesz w nim określić, że tworzy on ułamek zerowy0/1
. Tymczasem operatordefault
dlaBigRational
zawsze będzie zwracał0/0
- zawsze będzie miał pola zainicjowane domyślnymi wartościami typuBigInteger
(czyliNumerator = Denominator = 0
).Problem powoduje określone konsekwencje i wybory w kontekście projektowanego typu:
- Ułamek jest liczbą oraz liczby w C# reprezentowane są jako struktury ⇨
BigRational
też będzie reprezentowany jako struktura. - Skoro ułamek jest liczbą (niekoniecznie całkowitą), to liczba ta powinna być "kompatybilna" ze zmiennoprzecinkową reprezentacją (
float
,double
,decimal
). W szczególności, w typach tych zdefiniowano (w standardzie) takie sytuacje jakNaN
,PositiveInfinity
czyNegativeInfinity
oraz arytmetykę rozszerzoną na nieskończonościach. - W definicji typu musisz rozważyć:
◌0/0
nie ma matematycznego sensu, zatem taką wartość zaklasyfikujemy jakoNaN
. Tę wartość (a nie0
) zwróci operatordefault(BigInteger)
.
◌0/1
będziemy traktować jako wartość liczbową0
.
◌1/0
możemy interpretować jako +∞ zaś-1/0
jako -∞. Ogólnie każdy ułamek o mianowniku0
i liczniku niezerowym jest nieskończonością. Jeśli licznik jest dodatni - jest toPositiveInfinity
, jeśli ujemną - toNegativeInfinity
.
◌ Zaimplementuj metody statyczne:bool IsNaN
,bool IsInfinity
,bool IsNegativeInfinity
,bool IsPositiveInfinity
,IsFinite
- wzorując się na typiedouble
(patrz Double.IsNaN(Double) Method).
◌ Ułamki1/2
,2/4
czy3/6
reprezentują tę samą wartość (formalnie mówimy o relacji równoważności w zbiorze ułamków:$\frac{a}{b} = \frac{c}{d} ⇔ ad = bc, \quad b ≠ 0, d ≠ 0$ i klasach abstrakcji). Możemy się umówić, że wszystkie ułamki "tego samego typu" mają jednego reprezentanta - odpowiadający im ułamek nieskracalny.
- Ułamek jest liczbą oraz liczby w C# reprezentowane są jako struktury ⇨
-
Upraszczając ułamki skorzystasz z algorytmu Euklidesa obliczania NWD (ang. GCD). Nie znajdziesz go w klasie
System.Math
. Zatem:-
albo zaimplementujesz go samodzielnie, np. na podstawie informacji z Wikibooks
UWAGA: przed użyciem, sprawdź poprawność działania tego algorytmu dla rozwiązania Twojego problemu → np. jak zachowuje się dla liczb o różnych znakach.
-
albo skorzystasz z tego, dostarczonego w klasie
System.Numerics.BigInteger
.
Proces upraszczania należy umieścić w konstruktorach po to, by zapamiętany ułamek był już nieskracalny.
-
-
Zaimplementuj stałe ułamki:
NaN
(jako0/0
),Zero
(jako0/1
),One
(jako1/1
) orazHalf
(jako1/2
). -
Ponieważ testów jednostkowy dla Twojej klasy będzie dużo, rozbij je na wiele klas i plików. Dla potrzeb testowania podstawowej funkcjonalności z tego kroku, zmień nazwę klasy testującej np. na
BigRationalCoreUnitTests
. -
Aby usprawnić proces testowania, zamiast metody testującej z atrybutem
[TestMethod]
możesz stosować atrybut[DataTestMethod]
z podaniem w kolejnych, niższych wierszach, przykładowych zestawów testowych:[DataTestMethod] [DataRow(1, 3, 1, 3)] [DataRow(3, 1, 3, 1)] [DataRow(2, 4, 1, 2)] [DataRow(0, 2, 0, 1)] public void Konstruktor_PoprawneDaneBezUpraszczania_OK(int licznik, int mianownik, int expextedLicznik, int expectedMianownik) { // arrange - realizowane jako DataRow // act var u = new BigRational(licznik, mianownik); // assert Assert.AreEqual(u.Numerator, expextedNumerator); Assert.AreEqual(u.Denominator, expectedDenominator); }
Uwaga: W tym przypadku parametrami
DataRow()
muszą być literały całkowite (int
) - zatem testowanie przeprowadzisz tylko dla wartości tego typu danych. -
W języku C# stałe definiowane są za pomocą słowa kluczowego
const
. Definiowana stała musi być jasno określona lub możliwa do ustalenia jeszcze w trakcie kompilacji. W naszym przypadku zasymulujesz działanie stałej zmienną tylko do odczytu (prawdopodobnie użyjeszpublic static readonly
). Przeczytaj: C# Const, ReadOnly & Static ReadOnly Differences.Statyczne składniki klasy incjowane są w statycznym konstruktorze. Składniki zadeklarowane jako
static readonly
muszą być inicjowane albo w statycznym konstruktorze, albo jako część swojej deklaracji. Dokumentacja Microsoft zaleca, iż - jeśli nie ma potrzeby definiowania statycznego konstruktora w klasie - to składnikistatic readonly
inicjujemy w ich deklaracji, ze względów wydajnościowych.
Funkcjonalności z tej części mogą być zrealizowane już teraz, ale w niektórych przypadkach łatwiej będzie je zdefiniować równolegle, w kolejnych krokach (np. po implementacji równości ułamków czy operatorów rzutowania) - lub obecny kod później zrefaktoryzować.
-
Zaimplementuj konstruktor tworzący ułamek na podstawie tekstowej jego reprezentacji, tzn.
new BigInteger("-2/3") ⟶ numerator = -2, denominator = 3
. Konstruktor ten powinien być działaniem odwrotnym do metodyToString()
, tzn. jeśli utworzysz ułamek, następnie wyeksportujesz go do postaci tekstowej i ponownie utworzysz ułamek na jej podstawie, to otrzymasz "taki sam" ułamek:var u = new BigRational(1,2); var s = u.ToString(); var v = new BigRational(s); // u oraz v są "takie same"
Rozsądnym będzie wcześniejsze napisanie metod parsujących i wykorzystanie w konstruktorze.
-
Wzorując się na typie
int
(formalnieSystem.Int32
) zaimplementuj metodyBigRational Parse(string)
orazbool TryParse(string, out BigRational)
, które przetwarzają poprawnie uformowany napis do ułamka.Zastanów się i zaimplementuj zgłaszanie odpowiednich wyjątków.
-
Zaimplementuj konwersję
BigRational
do typudouble
(ToDouble()
),float
(ToSingle()
) orazdecimal
(ToDecimal()
). Musisz rozważyć dokładność konwersji. -
Zaimplementuj konstruktor
BigRational(double)
orazBigRational(decimal)
tak, aby korespondował z wcześniej opracowanymi konwersjami do tych typów. -
Zaimplementuj konwersję ułamka do typu
int
- z utratą informacji - będzie to wyznaczenie części całkowitej z dzielenia. -
Utwórz stosowne testy jednostkowe weryfikujące poprawność opracowanych metod.
-
W języku C# zwyczajowo, jeśli potrzebujemy tylko sygnatury metody (np. aby kod się poprawnie kompilował), a implementację pozostawiamy na później, zamiast kodu zgłaszamy wyjątek
NotImplementedException
. -
Do konwersji z
string
doBigRational
będziesz musiał parsować napis. Rozważ zastosowanie metody string.Split. Możesz również zastosować wyrażenia regularne (REGEX). -
Zadania dotyczące konwersji na inne typy liczbowe powtórzysz przy implementacji operatorów konwersji jawnej (rzutowanie) i niejawnej, w kolejnych krokach. Teraz możesz wykonać te implementacje i opracować testy jednostkowe. Później, gdy będziesz refaktoryzował kod, testy będą "pilnowały" jego poprawności.