Rozdíly v lambda výrazech v různých jazycích

7. 12. 2011 10:50 zboj

Podle posledního indexu Tiobe jsou nejpopulárnějšími jazyky Java, C, C++, C# a Objective-C. Je zajímavé porovnat implementaci a omezení lambda výrazů v těchto jazycích, člověk tak snáze porozumí, jak to uvnitř funguje.

Java

Java lambda výrazy nemá (existuje experimentální implementace, objevit se mají v osmé verzi). Má ale anonymní třídy, které je snadno nahradí. S tím se pojí určitá omezení. Chceme-li použít lokální proměnnou uvnitř lambda výrazu, musí být deklarována jako final:

final int n = 0; new Trida() { public int inc() { return n + 1; }}

Z toho samozřejmě plyne, že takovou proměnnou nemůžeme v anonymní třídě měnit. Toto omezení je dáno implementací, anonymní třídy totiž mohou přežít kontext, v němž byly vytvořeny, tedy po zániku proměnných na zásobníku. Díky final může runtime vytvořit kopii bez nebezpečí, že se data později změní. Pokud potřebujeme mít modifikovatelnou proměnnou, deklarujeme ji jako instanční proměnnou buď v anonymní třídě, nebo v třídě, v jejíž metodě jsme anonymní třídu vytvořili.

Pěkným příkladem použití anonymních tříd je AsyncCallback v GWT.

C

Podporu lambda výrazů má C v clangu (tam se nazývají bloky). Každý blok (zapsaný literálem) se vytvoří na zásobníku a se zásobníkem také zanikne. Pokud chceme blok zachovat (např. předat jako callback asynchronní metodě), musíme jej zkopírovat na haldu pomocí Block_copy (z block.h). Tím přebíráme odpovědnost za jeho odstranění z haldy funkcí Block_release.

Lokální proměnné se kopírují. Pokud chceme nějakou měnit, musí být deklarována pomocí __block. Takto deklarované proměnné jsou uloženy v objektu, který vnitřně reprezentuje blok. Zkopírujeme-li blok na haldu, tyto proměnné se zkopírují s ním. Zde je jistá podobnost s C# (lokální proměnná na haldě), narozdíl od C++ (viz dále) můžeme k modifikovatelným proměnným přistupovat i mimo kontext, v němž byl blok vytvořen.

C++

V clangu můžeme používat bloky z C (viz výše). Instance tříd se pro přístup z bloku kopírují (pokud nepoužijeme __block). V C++ se můžeme odpovědnosti za správu bloku na haldě zbavit jednoduchým wrapperem, tj. třídou, která přetíží operátor () a automaticky volá Block_copy a Block_release (taková třída bude podobná chytrým ukazatelům, Block_copy totiž kopíruje jen ze zásobníku na haldu, na haldě jen bloku zvýší čítač referencí, Block_release jej naopak snižuje, takže blok zanikne, až na něj neexistuje žádný odkaz). Takový wrapper má navíc tu výhodu, že je typu std::function<>, je tedy použitelný v std:.thread, std::async apod. Clangovské bloky lze přímo použít např. v Grand Central Dispatch, ale ne v STL.

Lambda výrazy v C++11 mají jinou syntaxi i implementaci. Jsou v podstatě jen syntaktickým cukrem nad funktory. Každá proměnná, kterou chceme ve výrazu použít, musí být explicitně uvedena (v „capture“ seznamu). Proměnná se buď zkopíruje (a pak ji můžeme použít i mimo kontext, v němž byl výraz vytvořen) nebo se na ni odkazuje referencí (a pokud jde o proměnnou na zásobníku, nelze ji mimo kontext použít). Z tohoto pohledu jsou clangovské bloky univerzálnější. Chceme-li psát přenositelný kód, musíme mít toto omezení na paměti.

C#

C# podporuje lambda výrazy od třetí verze. Modifikovatelné proměnné fungují podobně jako v clangu, ale není nutné uvádět __block nebo nějaký podobný modifikátor. Deklarujeme-li například int n = 0, vytvoří se taková proměnná většinou na zásobníku. Pokud ale překladač zjistí, že se používá v nějakém lambda výrazu, automaticky vytvoří tuto proměnnou na haldě. Pokud se tedy výraz použije i mimo lokální kontext, proměnná stále existuje (na zásobníku by zanikla), a navíc se vyhneme kopírování ze zásobníku na haldu. Jistou nevýhodou může být, že všechny proměnné jsou uvnitř výrazu modifikovatelné.

Objective-C

Pro ObjC platí to samé, co pro clangovské blogy v jazyce C. Je ale třeba dodat, jak se zachází s objekty.

Bloky se chovají jako standardní objekty, tj. místo Block_copy a Block_release můžeme použít zprávy copy a release. Odpovědnosti za správu živostnosti objektu se zbavíme pomocí autorelease: [[blok copy] autorelease] (copy zkopíruje objekt ze zásobníku na haldu a autorelease ho za nás v budoucnu z haldy odstraní).

Při použítí ARC se o bloky na haldě nemusíme starat vůbec, překladačem generované direktivy to udělají za nás.

Poznámka k objektům s čítačem referencí

ObjC a WinRT mají u objektů čítač referencí. Ukazatel na takový objekt se sémanticky chová spíše jako handle v Javě či C#. Při použití s lambda výrazy to znamená, že zatímco neobjektové proměnné se kopírují, objektům se pouze zvýší čítač referencí (a opět se sníží při zániku bloku).

Pokud tedy v C++/CX (WinRT) napíšeme

Trida^ obj = ref new Trida; [obj](){ obj->neco(); };

instanci třídy Trida se inkrementuje čítač, protože obj je uvedené v capture seznamu lambda výrazu. Pokud tomu chceme zabránit (například se chceme vyhnout cyklu referencí), použijeme [&obj], pak si ale samozřejmě musíme dát pozor na zánik použité instance, lokálně vytvořené objekt nepředaný jinam automaticky zanikne s lokálním kontextem (ref new sice vytváří objekty na haldě, ale handle po odstranění ze zásobníku sníží čítač referencí na nulu a zavolá se destruktor).

V Objective-C se chová čítač referencí u objektů podobně. Je třeba ale mít na paměti, že při použití ARC se čítač referencí aktualizuje i u proměnných deklarovaných pomocí __block, zatímco bez ARC ne. Pokud v ARC chceme zamezit vytvoření cyklu referencí, musíme místo __block použít __weak nebo __unsafe_unretained.

Sdílet