C# udostępnia kilka typów liczbowych, a każdy z nich reprezentuje inny zakres wartości. Przekazanie wartości pomiędzy różnymi typami wymaga konwersji, często będzie to konwersja wykonana automatycznie. Spójrzmy na przykład poniżej. W drugiej linijce wartość zmiennej i typu int
przypisywana jest do zmiennej fi typu float
. Niejawnie wykonywana jest konwersja wartości całkowitej na najbliższy jej odpowiednik wartości zmiennoprzecinkowej. Identyczna sytuacja następuje w dwóch ostatnich linijkach przykładu, wykonajmy kod w celu prostszego zrozumienia wykonywanych konwersji.
int i = 33;
float fi = i;
Console.WriteLine(i / 5);
Console.WriteLine(i / 5.0);
Console.WriteLine(fi / 5);
Pierwsza linijka wyświetla wynik dzielenia będący liczbą całkowitą. Podzielenie zmiennej całkowitej przez literał całkowity powoduje wykonanie operacji dzielenia całkowitego. Kolejna linia zawiera dzielenie wartości całkowitej przez literał zmiennoprzecinkowy, w tym przypadku to wartość zmiennej zostanie skonwertowana do postaci zmiennoprzecinkowej. Ostatnia linia to wykonanie dzielenia zmiennej fi typu float
przez literał całkowity. Przed wykonaniem tego dzielenia kompilator skonwertuje literał do wartości zmiennoprzecinkowej.
Ogólna zasada wykonywania obliczeń arytmetycznych na wartościach różnych typów liczbowych sprowadza się do wybrania typu o największym zakresie. Operatory arytmetyczne zazwyczaj wymagają, by operandy były tego samego typu, dlatego przed przystąpieniem do wykonywania operacji następuje niejawna konwersja typów.
Kompilator wykonuje niejawną konwersję wartości liczbowych, o ile typ docelowy będzie większego zakresu od typu źródłowego. Wcześniejszy przykład ograniczał się do wyświetlenia wyniku dzielenia. Kod poniżej, wykonuje operację dzielenia, w wyniku którego zwrócona zostanie wartość typu double
. Próba przypisania wyniku do zmiennej fi typu int
spowoduje komunikat o błędzie.
int i = 33;
double result = i / 5.0;
int fi = (int)result;
Jest to konwersja zawężająca, którą można wykonać wyłącznie jawnie. W tej sytuacji możliwe jest zastosowanie rzutowania, czyli określenia typu, do którego zmienna ma zostać skonwertowana. Rzutowanie wykonujemy poprzez podania typu wewnątrz nawiasów. (int)
. Warto zwrócić uwagę, że rzutowanie dotyczy wyłącznie wyrażenia umieszczonego bezpośrednio za nim. Próba wykonania takiego kodu int fi = (int)i / 5.0;
nie da żadnych efektów. Aby rzutowanie odnosiło się do wyniku operacji, należy je również otoczyć nawiasami w taki sposób: int fi = (int) (i / 5.0);
.
Konwersja polega na zmianie typu. W sytuacji, gdy może dojść do utraty informacji, wymagane jest wykonanie jawnej konwersji. Należy mieć na uwadze, że istnieją kombinacje typów, dla których określenie typu o większym zakresie jest trudne. Zastanówmy się jaki typ powinien zostać określony przy konwersji typu int
do typu uint
lub typu int
do float
. Każdy z typów posiada obszar pamięci 32-bitów, jednak zakres wartości poszczególnych typów jest różny. Typ uint
pozwala reprezentować wartość 3 000 000 001, co przekracza zakres typu int
, a typ float
może reprezentować wyłącznie jej przybliżenie. Język C# pozwala na niejawną konwersję, pomimo że potencjalnie mogą one doprowadzić do utraty danych. Pod uwagę brany jest wyłącznie zakres wartości, a nie precyzja poszczególnych typów. Przez co, choć typ float
nie pozwala na dokładną reprezentację wszystkich wartości typów int
oraz uint
, to pomimo tego umożliwia wykonanie niejawnej konwersji. Konwersja w przeciwnym kierunku nie będzie możliwa, typy int
oraz uint
nie zapewniają możliwości aproksymacji.
Zastanówmy się. Co się stanie w sytuacji przepełnienia danego typu liczbowego? Typy wartości udostępniają właściwości MinValue oraz MaxValue, określające minimalny oraz maksymalny zakres wartości. Spróbujmy zwiększyć o 1 maksymalną wartość typu int
(2 147 483 647).
int max = int.MaxValue;
int overflowed = max + 1;
W rezultacie zmienna overflowed otrzyma wartość -2 147 483 648, będącą minimalną wartością dla typu int
. Przykład prezentuje przekroczenie zakresu wartości dla typu int
. W przypadku liczb całkowitych wiąże się to z przejściem na drugi koniec zakresu. Nie trudno wyobrazić sobie sytuację, w której chcielibyśmy zostać poinformowani o takim zdarzeniu.
C# udostępnia słowo kluczowe checked
, wewnątrz którego możliwe jest umieszczenie wyrażenia, które w przypadku przepełnienia spowoduje zgłoszenie wyjątku OverflowException
. Zastosowanie checked
określamy jako kontekst sprawdzenia. Zastosowanie kontekstu sprawdzenia spowoduje, że operacje arytmetyczne, w tym rzutowanie, będą kontrolowane. Poniżej przykład zastosowania checked
. Warto zauważyć, że w przypadku zastosowania bloku checked
możliwe jest określenie wyrażenia. Możliwe jest również określenie wyrażenia, które nie zostanie objęte kontrolą, przez użycie unchecked
.
checked
{
int max = int.MaxValue;
int overflowed = max + 1;
}
checked
{
int max = int.MaxValue;
int overflowed = unchecked(max + 1);
}
Visual Studio umożliwia ustawienie parametru powodującego umieszczenie całego kodu w kontekście sprawdzania. W takiej sytuacji jedynie jawne zastosowanie unchecked
pozwoli ignorować incydenty związane z przepełnieniem. W celu włączenia klikamy prawym przyciskiem myszy w Solution Explorer i wybieramy Properties. Następnie klikamy w przycisk Advanced na zakładce Build i zaznaczamy opcję Check for arithmetic overflow / underflow.
Na koniec jeszcze jedna przydatna informacja. .Net Framework od wersji 4.0 udostępnia typ BigInteger
, który można używać identycznie jak pozostałe wbudowane typy liczbowe. Typ znajduje się w przestrzeni nazw System.Numerics
, którą należy dodać. Zgodnie z tym, co sugeruje nazwa, typ pozwala na przechowywanie wartości całkowitych, przy czym jego zakres może zostać dowolnie powiększony. Należy mieć jednak na uwadze, że praca z ogromnymi liczbami będzie wiązała się ze zwiększonym zapotrzebowaniem na pamięć operacyjną, a każda nawet najprostsza operacja będzie znaczącym obciążeniem procesora.
BigInteger bi = BigInteger.Parse(int.MaxValue.ToString());
bi += 1;
Troska Robert