Konwersje jawne, niejawne (definiowane i niedefiniowane przez użytkownika)

0

Witajcie,

Przerabiam dalej książkę o C# i dzisiaj natrafiłem na zadanko(teoretyczne) pod koniec rozdziału. Niestety ten rozdział był mało przystępnie wytłumaczony, dlatego też zwracam się do Was. Może ktoś mi to rozjaśni.

Treść zadanka:

W Twoim programie, poza innymi elementami, znajdują się dwie klasy Earth TimeSpan i BliposTimeSpan. Każda z tych klas ciągle zawiera zmienną instancji o nazwie totalSeconds typu uint. W tym przypadku EarthTimeSpan zawiera zdefiniowaną przez użytkownika jawną konwersję na typ short, a BliposTimeSpan zawiera zdefiniowaną przez użytkownika niejawną konwersję decimal na typ BliposTimeSpan. Zmienna myShort jest typu short, amyDecimal—typu decimal, zaś earthTimeSpanl jest typu EarthTimeSpan, a bliposTime5pan2 jest typu BliposTimeSpan.
Rozstrzygnij, która z poniższych instrukcji jest dozwolona:
a) myDecimal = bliposTimeSpan1;
b) myDecimal = (decimal) bliposTimeSpan1;
c) myShort = (short) bliposTimeSpan1;
d) myShort = earthTimeSpan1;
**e) myShort = (short)earthTimeSpan; **
f) myDecimal = earthTimeSpan;
g) myDecimal = (decimal)earthTimeSpan;
h) bliposTimeSpan1 = earthTimeSpan1;
__i) bliposTimeSpan1 = (short)earthTimeSpan1; __
j) bliposTimeSpan1 = (decimal)earthTimeSpan1;
k) earthTimeSpan1=bliposTimeSpan1;
l) earthTimeSpan1=(short) bliposTimeSpan1;
m) earthTimeSpan1= (decimal) bliposTimeSpan1;

Odpowiedzi poprawne to te które są podkreślone i pogrubione.

  1. Czy jest ktoś na tyle kompetentny, kto by wytłumaczył każdy przykład dlaczego poprawny(niepoprawny)?
  2. Czy konwersje dzielą się na jawne, jawne zdefiniowane przez użytkownika, niejawne, niejawne zdefiniowane przez użytkownika?
3

To takie minimalne wprowadzenie - konwersje dzielą się na 'jawne' (explicit) i 'niejawne' (implicit).
Jawna konwersja musi być oznaczona przez programistę za pomocą rzutowania, np. int i = (int)2.3f;
Niejawna konwersja nie musi, np. float f = 2;

Może na początek kod implementujący podany w książce przykład: (głupie zwracane wartości, ale to chwilowo nieważne):

    class ETS // EST == EarthTimeSpan
    {
        public static explicit operator short(ETS e) { return 0; }
    }

    class BTS // BTS == BliposTimeSpan
    {
        public static implicit operator BTS(double d) { return null; }
    }

No i teraz po kolei:

a) myDecimal = bliposTimeSpan1;

Nie ma konwersji do decimala z BTS - jest odwrotna. Nie ma co więcej pisać, dalej:

b) myDecimal = (decimal) bliposTimeSpan1;

Dokładnie to co powyżej, nie ma żadnej konwersji.

c) myShort = (short) bliposTimeSpan1;

Tym bardziej nie ma do shorta, nudne te przykłady :>

d) myShort = earthTimeSpan1;

Ech, znowu nic nowego...

e) myShort = (short)earthTimeSpan;

Jest konwersja (jawna) do shorta zdefiniowana przez nas - static explicit operator short(ETS e) - więc działa.

f) myDecimal = earthTimeSpan;

Ale nie ma niejawnej konwersji (więcej w następnym punkcie).

g) myDecimal = (decimal)earthTimeSpan;

O, coś ciekawszego.
Punkt 13.4.4 standardu, User Defined Explicit Conversions.
Jest dość długi, ale w podsumowaniu najciekawszych, z punktu widzenia przykładu, części:

  1. znajdywana jest 'najbardziej pasująca' konwersja (dokładne zasady opisane)
  2. magia :>
S = typ źródłowy (tutaj ETS)
T = typ deocelowy (tutaj decimal)
SX = najbliższy typ z którego wykonywana jest konwersja (tutaj ETS) - musi być konwertowalny za pomocą standardowej konwersji z S
TX = najbliższy typ do którego jest konwertowany (tutaj **short**)  - musi być konwertowany za pomocą standardowej konwersji do T

• Finally, apply the conversion:

o  If S is not SX, then a standard explicit conversion from S to SX is performed.  // S jest SX

o  The most specific conversion operator is invoked to convert from SX to TX.  // public static explicit operator short(ETS e)

o  If TX is not T, then a standard explicit [patrz - notatka] conversion from TX to T is performed. // standardowa konwersja short -> decimal

Notka - konwersja short -> decimal jest implicit, ale wszystkie standardowe konwersje implicit są z automatu explicit:

13.3.2 Standard explicit conversions

The standard explicit conversions are all standard implicit conversions plus the subset of the explicit
conversions for which an opposite standard implicit conversion exists. [Note : In other words, if a standard
implicit conversion exists from a type A to a type B, then a standard explicit conversion exists from type A to
type B and from type B to type A. end note]

Wtedy mamy coś takiego jak:
myDecimal = jakiś_decimal
Co jest oczywiście poprawne.

Czyli w skrócie, po ludzku - wykonywana jest najbardziej pasująca konwersja, i typ wynikający z tej konwersji jest dodatkowo konwertowany do typu ostateczniego (ETS->short->decimal) za pomocą niejawnej, standardowej konwersji (13.1.2, implicit number conversions).

13.1.2 Implicit numeric conversions

The implicit numeric conversions are:

(...)
From short to int, long, float, double, or decimal.
(...)

h) bliposTimeSpan1 = earthTimeSpan1;

ETS nie ma niejawnej konwersji do żadnego (ok, oprócz System.Object) typu, to po prostu nie może działać.

i) bliposTimeSpan1 = (short)earthTimeSpan1;

ETS jest konwertowany jawnie do short-a (żadne cuda), i teraz mamy sytuację taką
bliposTimeSpan1 = jakis_short;

Tutaj konwersja idzie jak w g), zmienia się końcówka. Mamy tam:

BTS = jakiś_decimal
Którą ofc da się prosto wykonać.

j) bliposTimeSpan1 = (decimal)earthTimeSpan1;

Praktycznie to samo co g, tylko ostatnia konwersja to nie standardowa konwersja short->decimal, tylko static implicit operator BTS(decimal d)

k) earthTimeSpan1=bliposTimeSpan1;
l) earthTimeSpan1=(short) bliposTimeSpan1;
m) earthTimeSpan1= (decimal) bliposTimeSpan1;

BTS nie ma żadnej konwersji do niczego (oprócz object ofc), koniec zabawy.


Ok, to 'kompetentne' odpowiedzi udzielone. Ale można do tego podejść 'luźniej', mniej formalnie...
Streszczenie wszystkich zasad:

  1. jeśli jest zdefiniowana w kodzie konwersja z typu A do B, wykonaj ją
  2. jeśli nie, znajdź najbliższą konwersję pośrednią, taką że A da się standardowo konwertować do AX, BX da się standardowo konwertować do B, a AX da się konwertować do BX (czyli ostatecznie A -> AX -> BX -> B).
  1. Czy konwersje dzielą się na jawne, jawne zdefiniowane przez użytkownika, niejawne, niejawne zdefiniowane przez użytkownika?

Nieścisłe trochę pytanie - standard wyróżnia konwersje 'użytkownika' i 'standardowe', oraz dzieli je na 'jawne' i 'niejawne'. Konwersje jawne i niejawne są z reguły tak samo traktowane, aczkolwiek w niektórych przypadkach wymagane jest żeby konwersja była standardowa (jak wyżej).

0

Powiedzmy, że w sumie wszystko zrozumiałem. To muszę się na pamięć wykuć możliwych konwersji. Jest gdzieś jakiś ciekawy rysunek przedstawiający możliwość konwersji?

0

Ale po co kuć na pamięć? Jeśli napiszesz:

typ nazwaZmiennej = zmiennaInnegoTypu;

i to zadziała, to konwersja jest możliwa. Jeśli nie będzie, to kompilator Cię o tym poinformuje błędem.

0

Żeby uniknąć błędów i od razu wiedzieć czy mam rzutować czy samemu jakoś napisać konwersję.

3

Rule of thumb:
dla konwersji standardowych: jeśli konwersja jest bezpieczna, tzn. nie powoduje w żadnym przypadku straty danych, jest niejawna -> np. zakres long > zakres int > zakres byte, czyli możesz bezpiecznie napisać

byte b = 5; // nawiasem mówiąc, tutaj zachodzi kolejna konwersja, tzw. `implicit constant expression conversion` - literał typu int jest konwertowany do byte.
int i = b;
long l = i;

Jeśli konwersja nie jest bezpieczna, tzn. typ docelowy ma mniejszy zakres, trzeba robić jawne rzutowanie

long l = 5;
int i = (int)l;
byte b = (byte)i;

W przypadku konwersji (jawnych/niejawnych) zdefiniowanych przez użytkownika z typu A do typu B można robić tylko (jawne/niejawne) rzutowanie z typu A do B i żadnego więcej.

Przy czym, jeśli jest rzutowanie powiedzmy niejawne z typu int do MojaKlasa, to pojawiają się jakby automatycznie rzutowania byte -> MojaKlasa, sbyte -> MojaKlasa, short -> MojaKlasa, ushort -> MojaKlasa - dlatego że byte, sbyte, short, ushort można niejawnie konwertować do int, więc są konwertowane i zachodzi konwersja z typu int do mojaKlasa.

0
D3X napisał(a):

Żeby uniknąć błędów i od razu wiedzieć czy mam rzutować czy samemu jakoś napisać konwersję.

Błędy kompilacji to nie jest problem, którego trzeba unikać, bo one nie powodują powstania złego programu. ;)

Swoją konwersję piszesz tylko w swoich klasach. Ale uwaga - to ma sens tylko w niektórych przypadkach, właściwie chyba tylko w przypadku operacji matematycznych - np. konwertowanie tablicy obiektów typu Wektor na Macierz, albo int czy double na obiekt klasy UłamekZwykły. Nie ma np. sensu konwertowanie obiektu Koszyk (w jakimś sklepie internetowym) na obiekt Faktura, w takim przypadku czytelniejsza i logiczniejsza będzie metoda. Chociaż, tego typu kwiatki z nadużywaniem własnych konwersji się czasem zdarzają.

0
msm napisał(a):

Jeśli konwersja nie jest bezpieczna, tzn. typ docelowy ma mniejszy zakres, trzeba robić jawne rzutowanie

long l = 5;
int i = (int)l;
byte b = (byte)i;

Jeśli zrzutujemy to nie będzie straty? W takim razie jak to działa? Możecie podać dosłowny przykład utraty danych i wyjaśnienia jak dzięki rzutowaniu zapobiega się temu?

0
long l = 9999999999999999L;
int i = (int)l;
0

Chodziło mi o podanie przykładu i opisanie go, bo to jest prawie to samo co zacytowałem od MSM

0
D3X napisał(a):

Jeśli zrzutujemy to nie będzie straty? W takim razie jak to działa? Możecie podać dosłowny przykład utraty danych i wyjaśnienia jak dzięki rzutowaniu zapobiega się temu?

Każdy typ danych ma swój zakres, np. int może przechowywać liczby od -2,147,483,648 do 2,147,483,647, a short od -32,768 do 32,767. Jak widać każda liczba typu short zmieści się w zmiennej typu int, ale nie odwrotnie.
Dlatego można dokonać niejawnej konwersji z short na int:

short x = 123;
int y = x;

Ale odwrotnie trzeba już rzutować:

int x = 242135;
short y = (short)x;

Bo jak widać, w tej sytuacji liczba x nie zmieści się w zmiennej y.

0

No właśnie i tyle jeszcze rozumiem. Tylko jak działa rzutowanie. W jaki sposób pozwala na konwertowanie int na short? Jeśli przecież zakresy na to nie pozwalają?

1

Jeśli zrzutujemy to nie będzie straty? W takim razie jak to działa? Możecie podać dosłowny przykład utraty danych i wyjaśnienia jak dzięki rzutowaniu zapobiega się temu?

Jak napisał somekind.

Konwersja short -> int jest bezpieczna, bo każdy short mieści się w incie. Czyli jeśli masz dowolny short, to możesz z niego zrobić poprawny int.
Konwersja int -> short nie jest bezpieczna, bo jeśli masz duży int, typu 70000, to nie można do reprezentować jako short.

To, co się stanie w takim przypadku:

int i = 70000;
short s = (short)i;

Zależnie od opcji kompilacji spowoduje rzucenie wyjątku OverflowException (jakoś tak w każdym razie) albo przekręceniem się shorta (będzie wynosił 4464 - patrz też http://en.wikipedia.org/wiki/Integer_overflow).

Popatrz na ten kod na przykład (maksymalna wartość reprezentowana przez ushort wynosi 65535):

static void Main(string[] args)
{
    int test = 65530;

    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("Int: {0}. Ushort: {1}", test + i, (ushort)(test + i));
    }
}
Int: 65530. Ushort: 65530
Int: 65531. Ushort: 65531
Int: 65532. Ushort: 65532
Int: 65533. Ushort: 65533
Int: 65534. Ushort: 65534
Int: 65535. Ushort: 65535
Int: 65536. Ushort: 0
Int: 65537. Ushort: 1
Int: 65538. Ushort: 2
Int: 65539. Ushort: 3

Kiedy ushort stał się za duży żeby go reprezentować w 16 bitach, 'przekręcił się' z powrotem do 0.

Jeszcze o rzutowaniu - rzutowanie nie zmienia nic w zachowaniu, to taka deklaracja ze strony programisty 'wiem co robię, jestem pewien że rzutowana wartość zmieści się w zakresie (i biorę odpowiedzialność za to co się stanie jeśli nie będzie)', w założeniu chroniąca przed przypadkową stratą dokładności.

0
somekind napisał(a):
D3X napisał(a):

Jeśli zrzutujemy to nie będzie straty? W takim razie jak to działa? Możecie podać dosłowny przykład utraty danych i wyjaśnienia jak dzięki rzutowaniu zapobiega się temu?

Każdy typ danych ma swój zakres, np. int może przechowywać liczby od -2,147,483,648 do 2,147,483,647, a short od -32,768 do 32,767. Jak widać każda liczba typu short zmieści się w zmiennej typu int, ale nie odwrotnie.
Dlatego można dokonać niejawnej konwersji z short na int:

short x = 123;
int y = x;

Ale odwrotnie trzeba już rzutować:

int x = 242135;
short y = (short)x;

Bo jak widać, w tej sytuacji liczba x nie zmieści się w zmiennej y.

Dobra. Tak jak somekind napisał. W ostatnim przykładzie nie da się skonwertować jawnie, bo wartość X wychodzi poza wartosc Y. I wywali błąd. Także co zmienia to rzutowanie. Ok tak jak pisaliście. Mówi, że na mojąodpowiedzialność bla bla bla. Ale co technicznie zmienia. Nagle da sie do shorta zapisac inta o takiej duzej wartosci?
Właśnie tego nie rozumiem.

1

Dobra. Tak jak somekind napisał. W ostatnim przykładzie nie da się skonwertować jawnie, bo wartość X wychodzi poza wartosc Y. I wywali błąd. Także co zmienia to rzutowanie. Ok tak jak pisaliście. Mówi, że na mojąodpowiedzialność bla bla bla. Ale co technicznie zmienia. Nagle da sie do shorta zapisac inta o takiej duzej wartosci?
Właśnie tego nie rozumiem.

...

int x = 242135;
ushort t = (ushort)x;
dec(242135) =          bin(111011000111010111)
uint x =     00000000000000111011000111010111 (32 bit)
ushort t =                   1011000111010111 (16 bit)
                           /\ strata 2 bitów danych.
0

A czy mógłby ktoś to wytłumaczyć nie na podstawie kodu tylko własnymi słowami?

242135 nie da się skonwertować na short, bo to za duża liczba. Wykraczająca poza zakres shorta.
Nagle! Dopisująć (short) do zmiennej int da się to skonwertować?

I wtedy np. short zmienna = (short)242135; będzie miała identyczną wartość czy właśnie nie?

0

Robiąc:
short zmienna = (short)242135;
Wykraczasz poza zakres typu short, przez co liczba 242135 zostanie skrócona.

0

No właśnie. Do ilu?

1

Zacytuję (wkleję) jeszcze raz swój poprzedni post:

int x = 242135;
ushort t = (ushort)x;
dec(242135) =          bin(111011000111010111)
uint x =     00000000000000111011000111010111 (32 bit)
ushort t =                   1011000111010111 (16 bit)
                           /\ strata 2 bitów danych.

To nie jest żaden kod w żadnym istniejącym języku, to obrazowe przedstawienie danych.

Reprezentacja liczby 242135 w systemie dwójkowym (powiedzmy że komputer ją tak widzi):

bin(242135) = 111011000111010111

Jest zapisana w incie, a jako że int ma 32 bity, reszta jest dopełniana zerami:

int i = 00000000000000111011000111010111 - to są 32 bity.

Przy czym, przypisujesz to do 16 bitowej liczby. Nie wszystkie bity się tam zmieszczą:

int i = 00000000000000111011000111010111

Te bity które się nie zmieszczą, są odrzucane. Te bity które się zmieszczą są zapisywane do 16 bitowego ushorta.

ushort s = 1011000111010111

Gdyby int był mały, czyli wszystko powyżej tych 16 bitów było zerem, nie byłoby strat. Ponieważ nit był duży, wartość ushorta nie jest równa intowi (to jest właśnie ta magiczna strata...)

1011000111010111 != 111011000111010111

Inaczej już nie wiem jak to wytłumaczyć, pomijając że podałem trzy kody i link których nawet nie przeanalizowałeś...

http://en.wikipedia.org/wiki/Integer_overflow
http://newbie.linux.pl/?id=article&show=297
http://pl.wikipedia.org/wiki/Przekroczenie_zakresu_liczb_ca%C5%82kowitych
http://www.tech-faq.com/integer-overflow.html
http://www.phrack.com/issues.html?issue=60&id=10
http://searchsoftwarequality.techtarget.com/definition/integer-overflow
https://www.owasp.org/index.php/Integer_overflow
http://kristiannielsen.livejournal.com/16392.html
http://intoverflow.wordpress.com/kinetic/
http://cwe.mitre.org/data/definitions/190.html
http://www.cs.utah.edu/~regehr/papers/overflow12.pdf
https://www.google.pl/search?q=integer+overflow

0
00000000000000000000000000000000
                0000000000000000

Typ short ma 16-bitów, typ int ma 32-bity.
Tak widzi to komputer (w uproszczeniu; przedstawiona wyżej liczba to oczywiście zero).

Liczba 242135 jest widziana w następujący sposób:

00000000000000111011000111010111
                1011000111010111

Jak widać, typ short nie pomieści wszystkich bitów i ostatecznie dwa pierwsze bity (11) zostaną utracone.
Więc ostatecznie otrzymałbyś liczbę: -20009.

W uproszczeniu, dlaczego liczba ujemna:
Mamy minus, ponieważ pierwszy bit jest zapalony.
Wykonujemy negację na wszystkich bitach.
Więc z tego:
1011000111010111
otrzymamy:
0100111000101000
Co po zamianie na system dziesiętny jest równe -20008-1=20009.
http://pl.wikipedia.org/wiki/Kod_uzupełnień_do_dwóch

0

I tłumaczenie Patryka rozumiem :) Dzięki

0
msm napisał(a):

(..)

Inaczej już nie wiem jak to wytłumaczyć
(..)

Dobrze tłumaczysz od samego początku, ale żeby to zrozumieć, trzeba mieć podstawy - wiedzieć, skąd się bierze int, long czy inny float, i co to bajt, słowo i przesunięcie bitowe oraz jak się poruszać w różnych arytmetykach (dwójkowych, piątkowych i innych -owych).

PS I nie, nie obrażam nikogo, tylko mówię, czemu ktoś tego może nie rozumieć.

0

@D3X, to nie jest problem języka, od którego zacząłeś. To kwestia wiedzy o tym jak "rozumie" liczby komputer: http://osilek.mimuw.edu.pl/index.php?title=WDP_Reprezentacja_liczb

1 użytkowników online, w tym zalogowanych: 0, gości: 1