Przejdź do głównej zawartości

[C#] Jak działają słowa kluczowe checked i unchecked?

W C# fragmenty programu mogą wykonywać się w dwóch kontekstach: checked i unchecked. Różnica polega na sposobie obsługiwania momentu przekroczenia zakresu liczbowego liczb całkowitych. Jak wiemy, każdy typ liczbowy ma swoje maksymalne i minimalne wartości. Na przykład:
  • dla int to -2 147 483 648 do 2 147 483 647 (int zapiany jest na 32 bitach, jeden bit przeznaczonyjest na znak (+/-), a zatem zakres to -(2^31) do 2^32-1)
  • dla byte to 0 do 255 (8 bitów, bez znaku, czyli 2^8-1)
  • dla short to -32 768 do 32 767 (16 bitów, jeden bit na znak, czyli zakres od -(2^16) do 2^16 -1)
Jeśli kod wywołuje się w kontekście checked przekroczenie zakresu danego typu liczbowego rzuci wyjątek ArtihmeticOverflowException. Jeśli kod wykonuje się w kontekście unchecked, przekroczenie zakresu "przekręci" wartość liczby (z int.MaxValue + 1 zrobi się minus int.MaxValue). Żaden wyjątek nie zostanie rzucony. Przyjrzyjmy się jak to działa w praktyce. Najpierw świadomie przekroczmy wartość int:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
using System;

namespace CSharpPractices
{
    class Program
    {
        static void Main(string[] args)
        {
            int number = int.MaxValue;
            number += 1;

            Console.WriteLine(number);
            Console.ReadKey();
        }
    }
}

Wynikiem działania tego programu będzie:

-2147483648

Teraz umieśćmy kod w kontekście checked. Okazuje się, że od rzuca wyjątek:
  Capture

Domyślnie kod wykonuje się w kontekście unchecked. Możemy to zmienić na poziomie ustawień kompilatora. W Visual Studio opcja ta znajduje się w Projekt -> Properties -> Build -> Advanced -> Check for arithmetic overflow/underflow. Będąc w kontekście checked możemy wykonań kod jako unchecked, umieszczając go w klauzuli unchecked:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

namespace CSharpPractices
{
    class Program
    {
        static void Main(string[] args)
        {
            checked
            {
                int number = int.MaxValue;
                unchecked
                {
                    number += 1;

                    Console.WriteLine(number);
                }
            }

            Console.ReadKey();
        }
    }
}

float

Uwaga: kontekst checked i unchecked dotyczy tylko liczb całkowitych. Liczby zmiennoprzecinskowe zachowują się inaczej. float nie rzuci nam wyjątku nawet w kontekście checked:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using System;

namespace CSharpPractices
{
    class Program
    {
        static void Main(string[] args)
        {
            checked
            {
                float number = float.MaxValue;
                number += 100000000000f;
                Console.WriteLine(number);
            }

            Console.ReadKey();
        }
    }
}

Wynik działania programu to...
3.402823E+38

...czyli float.MaxValue

decimal

Podobnie jak miało miejsce z float, także z decimal kontekst checked i unchecked nie ma znaczenia. Tyle, że w tym przypadku wyjątek poleci zawsze, niezależnie od tego, że jesteśmy w kontekście unchecked:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using System;

namespace CSharpPractices
{
    class Program
    {
        static void Main(string[] args)
        {
            decimal number = decimal.MaxValue;
            number += 1m;
            Console.WriteLine(number);            

            Console.ReadKey();
        }
    }
}

Co widać tutaj:

  Capture

Kiedy używać?

Używając kontekstu checked należy pamiętać, że sprawdzanie wartości liczb nie jest darmowe - jest to dodatkowa operacja, która pogarsza wydajność naszej aplikacji. Dlatego w większości codziennych zastosowań liczb, gdy nie przewidujemy realnego zagrożenia przekroczenia ich zakresu, lepiej trzymać się kontekstu unchecked. Oczywiście przekroczenie zakresu może być wynikiem błędu w kodzie. Dlatego niezłym pomysłem może być włączenie kontekstu checked dla aplikacji w buildzie debugowym, a wyłącznie go w buildzie releasowym. Istnieją dwie podstawowe sytuacje, kiedy przekroczenie zakresu jest spodziewane i jest częścią funkcjonowania programu:
  • przekroczenie zakresu ma być z definicji ignorowane przez pewne algorytmy. Są to na przykład algorytmy liczące sumy kontrolne, generatory liczb pseudolosowych czy algorytmy szyfrujące.
  • przekroczenie zakresu jest spodziewane, ale ma zostać obsłużone, a nie zignorowane. Przykładem może być prosta aplikacja typu kalkulator, która przekroczywszy zakres liczb ma po prostu poinformować o tym użytkownika, zamiast twierdzić, że 2 147 483 647 + 1 to -2 147 483 648

Pułapka

Używając słów kluczowych checked i unchecked, musimy pamiętać, że sprawdzenie wartości liczb ma miejsce tylko wewnątrz danej klauzuli. Dlatego jeśli w tej klauzuli zawołamy funkcję, jej wnętrze nie będzie już sprawdzane:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;

namespace CSharpPractices
{
    class Program
    {
        static void Main(string[] args)
        {
            int number = int.MaxValue;
            int power;
            checked
            {
                power = Power(number);
            }
           
            Console.WriteLine(number);            

            Console.ReadKey();
        }

        private static int Power(int number)
        {
            return number * number;
        }
    }
}

Kod ten działa bez rzucenia wyjątku, ponieważ kontekst checked nie dotyczy wnętrza funkcji Power. W tym poście wspierałam się:
  • https://www.codeproject.com/Articles/7776/Arithmetic-Overflow-Checking-using-checked-uncheck
  • https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/checked-and-unchecked

Komentarze