Pojďte s námi programovat !

1. 7. 2008

Sdílet

c# Prázdninové vydání programátorského kurzu jazyka C# 3.0 se věnuje problematice primitivních odkazových datov


c#

Prázdninové vydání programátorského kurzu jazyka C# 3.0 se věnuje problematice primitivních odkazových datových typů string a object.


Primitivní odkazové datové typy string a object

V jazykové specifikaci C# 3.0 a stejně tak i ve společném typovém systému (CTS) vývojově-exekuční platformy Microsoft .NET Framework 3.5 najdeme dva primitivní odkazové datové typy: string a object. V této lekci našeho programátorského kurzu se s nimi seznámíme. Kromě nezbytných informací si vysvětlíme rovněž průběh mechanizmu sjednocení typů.

Primitivní odkazový datový typ string

Jako první přijde na řadu datový typ string, což je primitivní odkazový typ jazyka C# 3.0, který nám umožňuje flexibilní a elegantní práci s textovými řetězci. Textové řetězce pro nás nejsou tak úplně neznámé, poněvadž jsme již měli možnost se setkat s řetězcovými konstantami (nebo literály). Řetězcová konstanta je ze syntaktického hlediska reprezentována korektně zapsanou posloupností textových znaků, které jsou uzavřeny do dvojitých uvozovek. Kdykoliv kompilátor diagnostikuje ve zdrojovém kódu řetězcovou konstantu, okamžitě podnikne kroky pro její uložení do objektu typu string.

V jazyce C# 3.0 můžeme řetězcovou konstantu přiřadit do odkazové proměnné, jejímž datovým typem je string:

static void Main(string[] args)
{
// Definiční incializace
// proměnné typu string.
string procesor =
"Intel Core 2 Duo";
}
Jak se již stalo naším dobrým zvykem, pečlivě a do všech detailů si představenou definiční inicializaci rozebereme. Tak předně, jelikož se jedná o definiční inicializaci, víme, že proměnná typu string bude nejenom definována, nýbrž také okamžitě inicializována určitou inicializační hodnotou. Definice proměnné se váže s alokací paměťového prostoru, který je vyhrazen ve speciální oblasti aplikační domény aplikace .NET. Tomuto prostoru se říká zásobník a jak jsme již uvedli, jde o automaticky spravovanou datovou strukturu, jejíž životní cyklus se řídí organizačním modelem LIFO. Alokované proměnné typu string, která je situována na zásobníku, budeme říkat odkazová proměnná. Je to proto, že datovým typem této proměnné je string, který se řadí k odkazovým datovým typům.

Dobrá, na zásobníku je vytvořen prostor pro odkazovou proměnnou s identifikátorem procesor. Pokud vás zajímá velikost alokovaného prostoru, pak vězte, že jde o čtyři bajty (předpokládáme standardní 32bitovou platformu). Prostor, který je pro odkazovou proměnnou k dispozici, má své symbolické pojmenování, jež vyjadřuje identifikátor proměnné.

Na pravé straně od přiřazovacího operátoru (=) stojí řetězcová konstanta, která sestává z posloupnosti textových znaků. Pokud se zaměříme pouze na syntaktické ztvárnění, pak to vypadá, jako by řetězcová konstanta byla přímo přiřazena do cílové odkazové proměnné. Ano, ačkoliv jazyk C# 3.0 používá tuto přirozenou syntaxi, ve skutečnosti je celý proces přiřazení daleko komplikovanější. A poněvadž každý správný programátor má dobrodružnou povahu, podíváme se, jak to je doopravdy. Nuže, když kompilátor po analýze zdrojového kódu zjistí, že chceme pracovat s řetězcovou konstantou, bude vědět, že musí realizovat následující akce:

Na řízené haldě je nutno vytvořit objekt, do něhož bude řetězcová konstanta zapouzdřena. Pojmem „řízená halda“ označujeme vyhrazenou oblast paměťového procesu aplikace .NET, do které jsou ukládány objekty. Ačkoliv ještě nenastal ten správný čas, abychom se pustili do rigorózní rozpravy o principech objektově orientovaného programování, musíme uvést několik základních postulátů. Především, objekt je entitou, jež zapouzdřuje data a množinu operací, které dovede s těmito daty provádět. Dále, objekt v našem chápání vystupuje jako logická jednotka, která má vlastnosti „černé skřínky“. Jako uživatelé nebudeme vidět „dovnitř“ objektu a tedy nebudeme schopni přímo říci, co objekt obsahuje. Objekt je svéprávnou jednotkou, což znamená, že dovede vykonávat určitou množinu činností (aktivit). Pro nás hraje v tuto chvíli roli zejména fakt, že objekt „ví“, jak tyto činností uskutečňovat.

Je rovněž velice důležité si uvědomit, že uživatelé využívají služeb objektu pouze přes jeho veřejně přístupné rozhraní. Konec konců, ve světě objektově orientovaného programování ani neexistuje jiná varianta, jak bychom mohli s objekty rozmlouvat.

V jazyce C# 3.0 musí každý objekt vzejít z jisté třídy. V případě objektu, do něhož bude uložena řetězcová konstanta, je touto třídou třída String z kořenového jmenného prostoru System. Kdykoliv kompilátor založí objekt (neboli instanci) třídy System.String, bude takovýto objekt schopen přijmout řetězcovou konstantu. Jakmile je instance třídy System.String na světě, kompilátor do ní uloží řetězcovou konstantu. Primitivní odkazový datový typ string jazyka C# 3.0 je ve skutečnosti pouze zástupným klíčovým slovem, které odpovídá třídě System.String. V zájmu zachování čistoty dalšího výkladu budeme tedy typ string a třídu System.String podle potřeby vzájemně zaměňovat.

Objekt zapouzdřující řetězcovou konstantu, jenž se nachází na řízené haldě, nemá své symbolické pojmenování a jako takový je anonymní. Abychom dovedli určit, kde objekt na řízené haldě skutečně sídlí, potřebujeme jednoznačnou identifikaci jeho pozice. Tu udává odkaz (neboli reference) na objekt. Tento odkaz nám kompilátor poskytne automaticky, přičemž my jej v rámci přiřazovacího příkazu ukládáme do definované odkazové proměnné. V této souvislosti považujeme za nesmírně důležité zdůraznit, že vytvořený objekt s řetězcovou konstantou je přímo dosažitelný pouze prostřednictvím vráceného odkazu. Neexistuje žádný jiný způsob, jak se k požadovanému objektu dostat, než použít odkaz identifikující dotyčný objekt. Pokud máte zkušenosti s programovacími jazyky C a C++, můžete na odkaz nahlížet jako na inteligentní a typově bezpečný ukazatel. Zatímco ukazatel je v jazycích C/C++ definován jako „paměťová adresa charakterizující pozici určitého datového objektu“, odkaz v jazyce C# 3.0 lze popsat coby „bezpečný prostředek pro identifikaci objektu určitého odkazového datového typu“. Fakt, že v jazyce C# 3.0 manipulujeme s odkazy, má praktické implikace. Jednou z nich je, že programátoři nikdy neznají přesnou paměťovou adresu, na které je objekt alokován. Podotkněme, že neznalost exaktní paměťové adresy není pro nás zásadní, neboť na tak nízké úrovní nebudeme v jazyce C# 3.0 takřka nikdy pracovat. Navíc o založené objekty pečuje automatický správce paměti, jenž samočinně uvolňuje již nepotřebné objekty.

Vzájemnou relaci mezi odkazovou proměnnou a objektem třídy System.String můžete vidět na obrázku.
Patrně už nyní chápete, proč má proměnná s identifikátorem procesor přívlastek „odkazová“. Kdykoliv použijeme tuto odkazovou proměnnou, automaticky bude dotázán cílový objekt, aby nám poskytl znění řetězcové konstanty. Mimochodem, každý textový znak řetězcové konstanty je reprezentován instancí primitivního hodnotového datového typu char. Můžeme proto prohlásit, že instance primitivního odkazového typu string je kolekcí instancí primitivního hodnotového typu char.

Inicializovanou odkazovou proměnnou typu string smíme použít kdekoliv uznáme za vhodné:
static void Main(string[] args)
{
string procesor =
"Intel Core 2 Duo";
Console.WriteLine(
"Můj počítač je " +
"osazen procesorem " +
procesor + ".");
}

Tento program vypíše na obrazovku informaci o procesoru. Z řetězcových konstant se dají skládat věty a z nich pak souvětí. Syntakticky probíhá zřetězení pomocí přetíženého aritmetického operátoru +. Ačkoliv za běžných okolností je operátor + symbolem pro sčítání hodnot svých operandů, při práci s textovými konstantami se chová jinak. Za těchto okolností operátor + správně vyhodnotí situaci a provede navázání jedné řetězcové konstanty na jinou. O operátoru říkáme, že je přetížen, když se jeho chování v různých kontextech liší.

Samozřejmě textové řetězce mohou přicházet také z jiných zdrojů, nejenom z pevně naprogramovaných řetězcových konstant. Tak kupříkladu další fragment zdrojového kódu jazyka C# 3.0 ukazuje, jak zjistit jméno počítače, na němž pracujeme:

static void Main(string[] args)
{
string počítač =
Environment.MachineName;
Console.WriteLine(
"Můj počítač se " +
"jmenuje {0}.",
počítač);
}

V tomto programu získáme pojmenování počítače pomocí vlastnosti MachineName třídy Environment ze jmenného prostoru System.
Jméno počítače je zjištěno z registrů operačního systému a v podobě textového řetězce je zapouzdřeno do objektu třídy System.String. Odkaz na vzniklý objekt je vzápětí uložen do připravené odkazové proměnné počítač typu string.

Objekt třídy System.String je neměnný, a to v tom smyslu, že může vždy obsahovat pouze jeden textový řetězec. Jestliže se pokusíme provést s objektem (a tedy samozřejmě se spřízněným textovým řetězcem) jistou manipulační operaci (jako je například zřetězení), kompilátor jazyka C# 3.0 bude generovat instrukce pro zrození nového objektu třídy System.String, jenž bude uchovávat novou řetězcovou konstantu (v případě zřetězení půjde o spojení dvou řetězcových konstant). Je zřejmé, že slabou stránkou tohoto pracovního modelu jsou jednak vyšší nároky na operační paměť a také vznik vedlejších režijních nákladů. Řešením je užití třídy StringBuilder ze jmenného prostoru System.Text. Pro potřeby této lekce programátorského kurzu si však vystačíme s instancemi primitivního odkazového datového typu string.

Textové řetězce můžeme porovnávat:

static void Main(string[] args)
{
string procesor1 =
"Intel Core 2 Duo";
string procesor2 =
"AMD Turion 64 X2";
if (procesor1 == procesor2)
Console.WriteLine(
"Procesory jsou " +
"shodné.");
else
Console.WriteLine(
"Procesory nejsou " +
"shodné.");
}

Komentář k programu: Ve zdrojovém kódu vytváříme dvě odkazové proměnné typu string, které okamžitě inicializujeme odkazy na objekty, jež obsahují produktová označení dvou procesorů. Z pohledu paměťového managementu existují na zásobníku dvě odkazové proměnné a na řízené haldě jsou uloženy dva objekty třídy System.String. Pokud chceme porovnat textové řetězce uložené v příslušných objektech, využijeme dvoucestné rozhodování pomocí rozhodovacího příkazu if-else. Tento příkaz jsme si představili v minulé lekci našeho programátorského kurzu, takže nyní se zaměříme pouze na kritická místa. Veškeré rozhodování je dáno vyhodnocením výrazu procesor1 == procesor2. Tento výraz je relačním výrazem, protože obě odkazové proměnné spojuje relační, přesněji porovnávací operátor, jenž je ztělesňován symbolem ==.

Výraz procesor1 == procesor2 testuje shodu textových řetězců, které jsou uloženy v objektech třídy System.String. Jinými slovy: operátor == zjišťuje, zda se textové řetězce shodují, nebo ne. V našem programu jsme použili různé procesory, tudíž řetězce se rovnat nebudou a hodnotou celého relačního výrazu bude logická nepravda (false).

Primitivní odkazový datový typ object

Zatímco primitivní odkazový datový typ string slouží pouze pro práci s textovými řetězci, typ object, o němž si budeme povídat nyní, má daleko širší pole působnosti. Klíčové slovo object plně substituuje primární bázovou systémovou třídu System.Object. Tato třída je pro programátory v jazyce C# 3.0 velmi důležitá, poněvadž je základem pro všechny ostatní datové typy. Technické detaily nechme v tuto chvíli stranou, protože jejich kompletní pochopení si vyžaduje mnohem více programátorských znalostí a dovedností. S typem object se pojí v jiných programovacích jazycích nevídané tajemství: do odkazové proměnné typu object mohou být uloženy jakékoliv hodnoty, ať už hodnotových, nebo odkazových datových typů.

Pokusme se namodelovat ukázku, v níž do odkazové proměnné typu object přiřadíme diskrétní celočíselnou konstantu:

static void Main(string[] args){
object o = 10;
Console.WriteLine("Hodnota "
+ "proměnné o: {0}.", o);
}
Proč je definiční inicializace odkazové proměnné o tak zajímavá? Je to proto, že za snad až magickou jednoduchostí příkazu se ukrývá úctyhodná směsice různorodých technických operací. Tento přiřazovací příkaz totiž aktivuje mechanizmus sjednocení typů.

Zmíněný mechanizmus musí být povolán, poněvadž na obou stranách přiřazovacího příkazu se nacházejí entity různých datových typů. Zatímco tedy na levé straně stojí proměnná, jejímž typem je odkazový typ object, na pravé straně je uvedena celočíselná konstanta, které kompilátor implicitně přiřadí primitivní hodnotový datový typ int. Kdykoliv, když kompilátor narazí na pokus o přiřazení hodnoty hodnotového typu do odkazové proměnné typu object, zabezpečí spuštění mechanizmu sjednocení typů.


Mechanizmus sjednocení typů uskutečňuje tyto činnosti:

Na řízené haldě je alokován prostor pro tzv. objektovou skříňku. Objektová skříňka je ve skutečnosti instancí podtřídy odvozené ze třídy System.ValueType.
Je sestrojena kopie hodnoty hodnotového datového typu (v našem případě kopie celočíselné konstanty), která je následně uložena do objektové skříňky.
Odkaz na vytvořenou a inicializovanou objektovou skříňku je uložen do odkazové proměnné typu object.

Popsaná triáda plně definuje všechny stěžejní aktivity, které vykonává mechanizmus sjednocení typů. Primárním úkolem tohoto mechanizmu je odstranit nekompatibilitu datových typů entit uvedených v přiřazovacím příkaze (odkazové vs. hodnotové typy). Mechanizmus sjednocení typů se nakonec pomocí chytře navrženého algoritmu typové nekompatibility zbaví, ovšem není to zadarmo. Ve skutečnosti se k činnosti mechanizmu sjednocení typů váže nemalá výkonnostní režie, neboť je nutné sestrojit objekt na řízené haldě, dále zhotovit kopii hodnoty hodnotového typu a tou posléze objekt inicializovat. I proto je nutné dbát na to, aby nemusel být mechanizmus sjednocení typů aktivován vícekrát, než je nezbytně nutné. V programovacím jazyce C# 3.0 je mechanizmus sjednocení typů aktivován implicitně, což znamená, že o provedení sjednocení typů nemusí vývojář explicitně žádat.


Průběh mechanizmu sjednocení typů si můžete prohlédnout na dalším obrázku.

Pokud budeme chtít z objektové skříňky získat hodnotu zpátky, zahájíme zpětný chod mechanizmu sjednocení typů:

static void Main(string[] args)
{
// Mechanizmus sjednocení
// typů.
object o = 10;
// Zpětný chod mechanizmu
// sjednocení typů.
int i = (int)o;
}
Zpětný chod mechanizmu sjednocení typů není prováděn automaticky, což znamená, že jeho realizaci si musejí vyžádat přímo programátoři. Syntakticky je zpětný chod mechanizmu sjednocení typů reprezentován explicitní typovou konverzí. Explicitní typovou konverzi představuje výraz (int)o, jenž se nachází na pravé straně druhého přiřazovacího příkazu. Explicitní typová konverze znamená konverzi hodnoty z původního typu na cílový typ. Cílový typ je uveden v závorkách a jak si můžeme všimnout, jde o primitivní hodnotový typ int. Pokud je explicitní typová konverze provedena v souvislosti s odkazovou proměnnou typu object, dojde k nastartování zpětného chodu mechanizmu sjednocení typů.

Zpětný chod mechanizmu sjednocení typů obnáší tyto činnosti:

Nejprve je provedena kontrola, zda na řízené haldě existuje objektová skříňka, v níž je zapouzdřena hodnota hodnotového datového typu.
Pokud je kontrola shledána jako úspěšná, vytvoří se kopie hodnoty hodnotového typu, která je umístěna v objektové skřínce.
Sestrojený duplikát je uložen do cílové hodnotové proměnné. 8 0363/CZ o

Grafickou ilustraci zpětného chodu mechanizmu sjednocení typů přináší další obrázek.

tipZpětný chod mechanizmu sjednocení typů je v originálních anglických zdrojích technických informací znám jako unboxing.upozorněníZ akademických zkušeností plyne, že studenti si poměrně často pletou přiřazovací (=) a porovnávací (==) operátor. Přestože ze syntaktického pohledu jde „pouze“ o přidání dalšího symbolu =, porovnávací operátor == se chová zcela jinak než jeho přiřazovací protějšek. Abychom neponechali prostor pro vznik nedorozumění, vyjasníme celou situaci okamžitě. Přiřazovací operátor uskutečňuje přiřazení hodnoty do proměnné. To je jeho jediný úkol, nic víc přiřazovací operátor nedělá. Na druhou stranu porovnávací operátor realizuje komparaci hodnot, které jsou nejčastěji uskladněny ve dvou proměnných. Hodnoty, které jsou předmětem porovnání, se buď rovnají, nebo ne. V závislosti na výsledku porovnání vrací porovnávací operátor buď logickou pravdu (hodnoty se rovnají), anebo logickou nepravdu (hodnoty se nerovnají). Technicky řečeno, výslednou hodnotou jakéhokoliv relačního výrazu bude logická hodnota true nebo false.Poznámka Samozřejmě, objekt není opatřen (alespoň prozatím ne) žádnou pokročilou umělou inteligencí. Jednoduše řečeno, veškeré vědění objektu bylo pevně naprogramováno jeho tvůrcem. Je tedy v kompetenci tvůrce objektu, aby stanovil, jaká data bude moci objekt obsahovat a jaké činnosti bude schopen uskutečňovat. Jak se později dozvíme, v jazyce C# 3.0 vznikají objekty z tříd, které vystupují jako uživatelsky deklarované odkazové datové typy. Třída je abstraktní reprezentací objektu, tedy jakousi šablonou, ze které je objekt následně vytvořen.