Přestaňte používat as jako cast

Tomáš Holan       13.02.2011       C#       11419 zobrazení

(V angličtině by název tohoto příspěvku asi vyzněl lépe “Stop using as as cast”.)

Dnes se pokusím, snad jednou provždy, vyřešit problém této až přespříliš časté konstrukce (také lze označovat jako anti-pattern). Ve skutečnosti na ní opravdu narážím téměř pořád a to i kdekoliv různě na webu a je přitom úplně jedno, zda se jedná o technologii ASP.NET, WPF nebo Silverlight.

var button = sender as Button;
button.Focus();

Jednoduše řečeno, výše uvedený kód je chybný. Proč? Vždyť to funguje nebo snad ne? Mě to ale fakt funguje, tak co je na tom jako špatně! OK tak se na to tedy naprosto nezaujatě ještě jednou koukneme: 

Tak máme tu v proměnné sender nějaký objekt, který může ale i nemusí být typu Button, tak zkusíme, jestli to tedy náhodou ten Button není a pak na tento objekt v případě, že je to opravdu…, počkat, moment… á, moje chyba!!! vždycky@!, tedy znovu: a pak na tento objekt zavoláme, a to i v případě, že to ten Button není (asi se nakonec ukazuje, že to, že by to někdy Button nebyl, tu nikdo asi předpokládat nemá – no to jsem mohl taky vědět hned), metodu… (uf).

Necháme-li úplně všechno stranou, jde o to, že operátor as a operátor cast jsou sémanticky odlišné. Pokud by jsme správně použiti operátor cast

var button = (Button)control;
button.Focus();

říkáme (*): “Já na 100% vím, že tento výraz typu B je  ve skutečnosti typu D (kde D je typ odvozený od B) a pouze chci, aby i kompilátor tuto mou informaci zohlednil (a pokud nemám pravdu, jsem smířen s tím, že bude v runtime vyhozena výjimka).

Zatímco tím, že jsme ale použili operátor as říkáme: “Tento výraz typu B může i nemusí být odvozeného typu D a na tomto místě chci pouze provést test, zda tomu tak náhodou není. Ať už test dopadne jak chce, v obou případech se následně podle toho zařídím.” (A ve splnění té poslední části jsme přitom bezesporu selhali.)

S tím ale souvisí také, a podle mého názoru dost významný, rozdíl i ve funkčnosti. Pokud bude objekt v uvedeném příkladu skutečně typu Button, je pravda, že to dopadne stejně, ale co když nebude? Oba případy sice nakonec skončí vyhozením výjimky, ale problém je, že jiné, a co je snad ještě horší, že u varianty s as dojde k jejímu vyhození jinde – později. Tedy místo InvalidCastException už při vlastním přetypování dostaneme naprosto nelogicky jen NullReferenceException a navíc až na použití výsledku chybně provedeného přetypování. Takže co třeba v případě, kdy mezi prvním a druhým řádkem z našeho příkladu bude nějaký další kód a na proměnnou s výsledkem se odkážeme až i třeba o hodně později, a co třeba v případě, že výsledek operátoru as předáme někam dál, pak bude výjimka vyhozena třeba i z úplně jiné metody, a co třeba v případě… Odladění takových případů pak může být opravdu dlouhá noc.

Navíc nehledě na to, že NullReferenceException je podle mého názoru jedna z nejčastějších výjimek (**) a v produkčním prostředí navíc i velmi obtížně odladitelná. Proč proto přidávat do kódu další místa, kde při chybě úplně jiné (a pravděpodobně ve většině případech naopak překvapivě snadno odladitelné) se zcela zbytečně dočkáme jen hlášky “Object reference not set to an instance of an object” nebo ještě lépe “Odkaz na objekt není nastaven na instanci objektu” v případě českého .NET Frameworku. Samozřejmě tím netvrdím, že by nikdy za nějakých okolností nemohlo nastat, že původní výraz, který se snažíme přetypovat, měl sám o sobě hodnotu null, ale takový chybový případ a chybné přetypování chci mít rozhodně odlišený.

A když už jsme u toho, operátory as a cast jsou odlišné i v mnoha dalších věcech, jako to, že operátor as lze použít pouze u referenčních nebo nullable typů a nelze kromě přetypování použít tak jako operátor cast i pro konverzi datových typu.

Teď by se po tom všem co bylo uvedeno mohlo skoro zdát, že operátor as je k ničemu, v jakém případě je tedy jeho použití správně? No v případech, kdy nám právě vyhovuje jeho sémantika tj. když máme výraz, který opravdu může nabývat různých datových typů.

var control = element as Control;
if (control != null)
{
    control.Background = background;
    return;
}
var panel = element as Panel;
if (panel != null)
{
    panel.Background = background;
    return;
}
//...

Toto je pak samozřejmě optimálnější, než nejprve v prvním kroku testovat datový typ pomoci operátoru is a v kroku druhém teprve provést vlastní přetypování pomoci cast.

(POZOR: Toto je neoptimální kód.)

if (element is Control)
{
    var control = (Control)element;
    control.Background = background;
    return;
}
//...

Také jsem ale četl názor, že nutnost takového testování různých datových typů, je známka slabého designu hierarchie tříd a její nápravou spolu s polymorfizmem by mělo jít takovéto nutnosti zabránit. Ale svět není ideální a někdy musíme používat i třídy, které jsme nenapsali my sami apod. (např. ve výše uvedeném kódu pochází třídy Control a Panel ze Silverlightu, obě dědí rovnou ze základní třídy FrameworkElement, která vlastnost Background samozřejmě mít nemůže).

Závěrem: Vždy přemýšlejte i nad sémantikou vašeho kódu.


(*) Operátor cast má ve skutečnosti dvě odlišné (a se dá říct, že i přímo navzájem opačné) základní použití: jako přetypování a jako konverze. Pro bližší vysvětlení vřele doporučuji přečíst tento článek (část začínající: “There are two (¤) basic usages of the cast operator in C#”). V našem případě se zde jedná pouze o použití jako přetypování.

(**) A ano i já bych chtěl non-nullable types (ale to však v již existujícím jazyce není technicky možné).

 

hodnocení článku

1 bodů / 3 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback