Cuprins >> Obiecte > Namespace
Articol de importanță mare

În OOP (Object Oriented Programming - Programarea Orientata pe Obiecte), namespace-urile sunt containere pentru un grup de clase care au un context comun sau sunt clasificate prin funcționalitate comună. Namespace-urile nu au niciun fel de funcționalitate și nu este obligatoriu să le folosiți. Cu toate acestea, ele oferă câteva avantaje, cel mai important fiind abilitatea de a sorta și grupa codul în unități logice.

Să luăm în considerare următorul exemplu: vrem să creăm două obiecte în cod – un robot și un om. Destul de ușor de realizat, până când descoperim că am creat clasa Mana pentru om, dar nu putem crea o altă clasă Mana pentru robot, din cauza unui conflict de nume. Proiectul conține deja o clasă cu acest nume. Cum ar putea diferențieze ce clasă Mana am intenționat să menționăm – a clasei om, sau robot? Și, nu, nu putem folosi aceeași clasă, una e facută din carne și are muschi, cealaltă este facută din metal și conține motorașe.

Deși acest lucru nu este obligatoriu în C#, este considerată o practică bună să avem clase cu nume identice cu al fișierelor care le conțin, iar namespace-urile ar trebui să aibă același nume ca și dosarele în care locuiesc. În Java, acest lucru este chiar un proces obligatoriu, nu veți putea să vă compilați codul dacă nu respectați această regulă.

Am lucrat cu namespace-uri înainte, de fapt le-am văzut în fiecare dintre lecțiile noastre, deoarece Visual Studio le adaugă automat atunci când creăm o nouă soluție, proiect sau fișier. Puteți testa acest lucru prin adăugarea unei noi clase la proiectul vostru, așa cum am făcut aici. Dacă analizați codul, veți observa că dacă ați adăugat o clasă numită ClasaMea, într-un proiect numit ProiectulMeu, codul din această clasă va arăta astfel:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class ClasaMea
    {
    }
}

Și putem instanția această nouă clasă astfel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            ClasaMea clasaMea = new ClasaMea();
        }
    }
}

Ceea ce nu știți este că numele calificat (întreaga adresă) a clasei este de fapt acesta:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            ProiectulMeu.ClasaMea clasaMea = new ProiectulMeu.ClasaMea();
        }
    }
}

deoarece ClasaMea a fost declarată în namespace-ul ProiectulMeu, prin urmare întreaga sa adresă este ProiectulMeu.ClasaMea. Motivul pentru care compilatorul ne permite să o instanțiem fără a specifica namespace-ul este acela că o instanțiem într-o altă clasă, și ea declarată în același namespace. Și întrucât un namespace poate conține doar o singură clasă cu un nume dat, nu este necesar să se precizeze namespace-ul atunci când folosim acest nume de clasă, compilatorul știe deja că doar o singură clasă cu acest nume poate exista în interiorul său.

Acum, să declarăm noua noastră clasă într-un alt namespace. Modificați numele namespace-ului după cum urmează:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace NumeleNamespaceuluiMeu
{
    class ClasaMea
    {
    }
}

După cum puteți vedea, nu am schimbat foarte mult codul, doar am redenumit namespace-ul. Dar acum, această bucată de cod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            ProiectulMeu.ClasaMea clasaMea = new ProiectulMeu.ClasaMea();
        }
    }
}

sau această bucată de cod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            ClasaMea clasaMea = new ClasaMea();
        }
    }
}

va genera o eroare de compilator: Numele sau namespace-ul ‘ClasaMea’ nu există în namespace-ul ‘ProiectulMeu’ (lipsește o referință de asamblare?) (The type or namespace name ‘ClasaMea’ does not exist in the namespace ‘ProiectulMeu’ (are you missing an assembly reference?)). Acest lucru se întâmplă deoarece am redenumit namespace-ul în care este declarată ClasaMea, iar când încercăm să o instanțiem în clasa Program declarată în namespace-ul ProiectulMeu, compilatorul vede că nu există o clasă numită ClasaMea declarată în interiorul unui namespace numit ProiectulMeu.

Pentru a scăpa de această eroare, trebuie să modificăm adresa namespace-ului:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            NumeleNamespaceuluiMeu.ClasaMea clasaMea = new NumeleNamespaceuluiMeu.ClasaMea();
        }
    }
}

Trebuie să rețineți întotdeauna că atunci când apelați o clasă declarată într-un alt namespace, altul decât cel în care faceți apelul, trebuie să utilizați nume complet calificate (inclusiv namespace). În cuvinte simple, dacă aveți un namespace A, care conține clasele X și Y, X poate folosi direct Y, folosind doar numele clasei; dar dacă aveți două namespace-uri, A conținând clasa X, și B conținând clasa Y, clasa X poate folosi clasa Y doar prin specificarea namespace-ului în care este declarată Y, și viceversa.

De asemenea, amintiți-vă: clasele trebuie să aibă nume unice numai în namespace-urile în care sunt definite. Putem avea două clase cu același nume, atâta timp cât sunt declarate în namespace-uri diferite (acesta este și motivul primar pentru folosirea namespace-urilor).

Namespace-urile pot fi de asemenea, imbricate. Aceasta este o modalitate ușoară de a crea o ierarhie logică, care permite o distribuție și mai precisă a claselor. Calea namespace-urilor imbricate este specificată utilizând operatorul . (punct). Să luăm în considerare următorul exemplu:

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.Collections.Generic;
 
namespace NumeleNamespaceuluiMeu
{
    class ClasaMea
    {
        System.Collections.Generic.List<string> listaMea = new System.Collections.Generic.List<string>();
    }
}
 
namespace System
{
    namespace Collections
    {
        namespace Generic
        {
            public class List<T>
            {
            
            }
        }
    }
}

Am declarat un namespace System, care conține un namespace Collections, care conține un namespace Generic, care conține o clasă generică numită List. Apoi, am declarat un alt namespace denumit NumeleNamespaceuluiMeu, care conține o clasă numită ClasaMea, în interiorul căreia dorim să instanțiată clasa List pe care am declarat-o anterior.

Așa cum am spus mai sus, deoarece clasa pe care dorim să o instanțiem și clasa în care dorim să instanțiem se află în namespace-uri separate, trebuie să specificăm numele complet calificat:

1
System.Collections.Generic.List<string> listaMea = new System.Collections.Generic.List<string>();

Începeți să vedeți semnificația acestor nume lungi?

1
2
3
4
5
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Da, aveți dreptate; acestea sunt namespace-uri și namespace-uri imbricate. E timpul potrivit și pentru a explica directivele using.

IAm spus deja că atunci când declarăm două clase în același namespace, nu trebuie să specificăm numele namespace-ului atunci când instanțiem o clasă din cealaltă clasă, deoarece compilatorul presupune deja că facem referire la clasa declarată în același namespace. Cu toate acestea, dacă clasa este declarată într-un alt namespace, trebuie să folosim numele complet calificat. Considerați acest cod:

1
2
3
4
5
6
7
8
9
10
11
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Int32 variabilaInt = 3;
            System.String variabilaString = "aceasta este o declaratie de string, folosind numele complet calificat";
        }
    }
}

Observați diferența față de modul obișnuit în care arată codurile noastre? În primul rând, nu avem directive using care să fie utilizate în partea de sus a fișierului nostru. În al doilea rând, am folosit calea complet calificată pentru declarațiile variabilelor int și string. Din aceste două modificări, putem trage următoarea concluzie: directivele using sunt utilizate doar pentru a putea folosi notația scurtă a tipurilor și claselor.

Informații Adiționale

Este posibil să fi observat utilizarea System.Int32 și System.String în numele complet calificat, în loc de System.int și System.string. Acest lucru se datorează faptului că int și string sunt de fapt doar aliasuri pentru System.Int32 și System.String, după cum am explicat aici.

Calea complet calificată a clasei Console este System.Console. Deci, când vrem să scriem ceva pe consolă, ar trebui să o facem în acest mod:

1
2
3
4
5
6
7
8
9
10
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("Test");
        }
    }
}

dar întotdeauna scriem așa:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
 
namespace ProiectulMeu
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Test");
        }
    }
}

Cuvântul cheie using specifică faptul că programul folosește numele dintr-un namespace dat, în cazul nostru, System. Și deoarece clasa Console este declarată în namespace-ul System, nu este nevoie să specificăm întreaga cale, deoarece linia using System; spune deja programului nostru că atunci când apelăm o clasă, ar trebui să caute numele acelei clase și în interiorul namespace-ului System.

Există însă o consecință a acestui fapt. Luați în considerare următorul cod:

1
2
3
using System.Windows.Forms;
using System.Web.UI;
using System.Windows.Controls;

Fiecare dintre namespace-urile de mai sus conține o clasă numită Control. Chiar dacă am importat respectivele namespace-uri cu ajutorul cuvântului cheie using, cum ar putea ști compilatorul la care din ele am făcut referire, dacă vrem să instanțiem clasa Control utilizând notația scurtă? Nu ar ști, și ar genera de fapt această eroare: ‘Control’ este o referință ambiguă între ‘System.Windows.Controls.Control’ și ‘System.Windows.Forms.Control’ (‘Control’ is an ambiguous reference between ‘System.Windows.Controls.Control’ and ‘System.Windows.Forms.Control’). În cazul în care aveți clase cu aceleași nume, chiar dacă utilizați declarații using, trebuie să specificați numele complet calificat, astfel încât compilatorul să știe la ce clasă ați făcut referire.

În cele din urmă, țineți cont de faptul că directivele using NU sunt recursive! Folosirea instrucțiunii using System.Windows; nu implică și importarea System.Windows.Forms, deși namespace-ul System.Windows.Forms este declarat în interiorul namespace-ului System.Windows.

Atunci când aveți namespace-uri lungi sau folosite des, C# vă oferă aliasuri de namespace. Acestea pot ajuta la îmbunătățirea lizibilității codului și reducerea riscului de conflicte de nume, în special atunci când lucrați cu mai multe biblioteci care au nume de tipuri similare. Pentru a crea un alias de namespace, folosiți cuvântul cheie using , urmat de un nume de alias, un semn egal (=) și namespace-ul complet calificat pe care doriți să-l creați ca alias.

De exemplu, imaginați-vă că aveți următoarele namespace-uri:

1
2
using System.Data.SqlClient;
using System.Data.SQLite;

Ambele namespace-uri conțin o clasă numită Command. Pentru a evita ambiguitatea și a îmbunătăți lizibilitatea, puteți crea aliasuri de namespace-uri astfel:

1
2
using SqlClient = System.Data.SqlClient;
using SQLite = System.Data.SQLite;

Acum, când trebuie să faceți referire la clasa Command din oricare dintre namespace-uri, puteți utiliza aliasul:

1
2
SqlClient.Command sqlCommand = new SqlClient.Command();
SQLite.Command sqliteCommand = new SQLite.Command();

În plus față de aliasurile de namespace-uri, C# oferă și calificatorul de alias de namespace (namespace alias qualifier), sau operatorul "::". Calificatorul de alias de namespace este utilizat pentru a accesa tipuri sau membri în cadrul unui namespace care a fost creat ca alias.

De exemplu, având în vedere aceleași aliasuri de namespace-uri ca în exemplul anterior:

1
2
SqlClient::Command sqlCommand = new SqlClient::Command();
SQLite::Command sqliteCommand = new SQLite::Command();

Această sintaxă face clar faptul că accesați clasa Command printr-un alias de namespace, în loc de un namespace obișnuit. Cu toate acestea, este important de remarcat că în codul C# calificatorul de alias de namespace este folosit mai rar, deoarece notația standard cu punct (.) este de obicei preferată pentru lizibilitate. Operatorul "::" poate fi util în cazurile în care trebuie să diferențiați între un alias și un tip sau membru cu același nume.

C# 10 a introdus conceptul de namespaces cu domeniu de aplicare la nivel de fișier, care reprezintă o modalitate mai concisă de a declara un namespace, care cuprinde întregul fișier. Namespace-urile cu domeniu de aplicare la nivel de fișier pot ajuta la reducerea nivelului de indentare și la îmbunătățirea lizibilității codului. Deci, acest lucru:

1
2
3
4
5
6
7
8
using System;
 
namespace BunaLume
{
    class Program
    {
    }
}

devine acest lucru:

1
2
3
4
5
6
7
using System;
 
namespace BunaLume;
 
class Program
{
}

Observați punctul și virgula de după declarația namespace. Această modificare permite ca definiția BunaLume să existe la nivelul superior, părinte, fără nicio indentare suplimentară. Namespace-ul cu domeniu de aplicare la nivel de fișier se aplică tuturor tipurilor și membrilor definiți în fișierul în care este declarat.

Țineți cont că nu puteți amesteca namespace-uri cu domeniu de aplicare la nivel de fișier și namespace-uri cu domeniu de aplicare la nivel de bloc în același fișier. Dacă alegeți să utilizați namespace-uri cu domeniu de aplicare la nivel de fișier, toate namespace-urile din acel fișier trebuie să urmeze aceeași convenție.

C# 10 a introdus de asemenea și conceptul de using-uri implicite, cunoscute și sub denumirea de using-uri globale. Această funcționalitate permite includerea automată a directivelor using comune în proiectul vostru fără a fi nevoie să le scrieți explicit în fiecare fișier. Acest lucru poate duce la un cod mai curat și o mai bună lizibilitate, în special în proiecte mari.

Pentru a utiliza using-uri implicite, desigur, trebuie să folosiți cel puțin versiunea C# 10, dar trebuie să creați și un fișier nou, de obicei denumit Usings.cs sau GlobalUsings.cs în proiectul vostru. În interiorul acestui fișier, puteți declara directivele using pe care doriți să le aplicați la nivel global, prefixate cu cuvântul cheie global:

1
2
3
global using System;
global using System.Collections.Generic;
global using System.Linq;

După aceasta, nu mai trebuie să includeți directivele using specificate în alte fișiere din proiectul vostru, deoarece acestea sunt incluse implicit. Cu toate acestea, puteți adăuga în continuare directive using explicite în fișierele individuale, dacă este necesar.

Rețineți că, deși utilizarea implicită a using-urilor vă poate face codul mai simplu și poate îmbunătăți lizibilitatea, aceasta poate crește, de asemenea, riscul conflictelor de nume, așa cum am explicat anterior. Ca regulă generală, fiți precauți atunci când includeți multe using-uri globale, deoarece puteți introduce accidental referințe ambigue la tipuri sau namespace-uri.

Un alt efect negativ ar putea fi introducerea de magie în codurile voastre, adică lucrurile funcționează, deși, aparent, nu ar trebui să o facă. Utilizați Console.WriteLine() în codurile voastre, dar nu există niciun using System; în partea de sus a fișierului vostru. Motivul real, desigur, este că îl utilizați într-un using global, dar un începător, sau cineva nefamiliarizat cu acest concept, ar putea să întâmpine dificultăți în a înțelege cum funcționează acest lucru. Magia poate fi minunată în viața reală, dar este de nedorit în programare. Cu cât lucrurile funcționează mai evident, cu atât mai bine. Cu cât trebuie să faceți mai puțină muncă de detectiv pentru a înțelege cum funcționează codul, cu atât mai bine..

Încă în legătură cu namespaces (și nu numai), în lecția următoare voi discuta conceptul de top level statements (declarații de nivel superior), unde nu aveți nevoie nici de un namespace, nici de o clasă, și de ce acest lucru este rău.