Język Dart: kiedy Java i C# nie są wystarczająco ostre
Opublikowany: 2022-03-11Już w 2013 roku oficjalna wersja Darta w wersji 1.0 zyskała popularność – podobnie jak w przypadku większości ofert Google – ale nie wszyscy byli tak chętni, jak wewnętrzne zespoły Google, do tworzenia aplikacji o znaczeniu krytycznym dla biznesu w języku Dart. Dzięki dobrze przemyślanej przebudowie Dart 2 pięć lat później, Google wydawało się, że udowodniło swoje zaangażowanie w język. Rzeczywiście, dzisiaj nadal zyskuje na popularności wśród programistów — zwłaszcza weteranów Javy i C#.
Język programowania Dart jest ważny z kilku powodów:
- Ma to, co najlepsze z obu światów: jest skompilowanym, bezpiecznym językiem (takim jak C# i Java) i jednocześnie językiem skryptowym (takim jak Python i JavaScript).
- Transpiluje się do JavaScript w celu wykorzystania jako interfejs sieciowy.
- Działa na wszystkim i kompiluje się do natywnych aplikacji mobilnych, dzięki czemu można go używać prawie do wszystkiego.
- Dart jest podobny do C# i Java w składni, więc jest szybki do nauczenia.
Ci z nas ze świata C# lub Java większych systemów korporacyjnych już wiedzą, dlaczego bezpieczeństwo typów, błędy w czasie kompilacji i linter są ważne. Wielu z nas waha się przed przyjęciem języka „skryptowego” z obawy przed utratą całej struktury, szybkości, dokładności i możliwości debugowania, do których jesteśmy przyzwyczajeni.
Ale wraz z rozwojem Darta nie musimy z tego rezygnować. Możemy napisać aplikację mobilną, klienta internetowego i zaplecze w tym samym języku — i uzyskać wszystko, co nadal kochamy w Javie i C#!
W tym celu przejrzyjmy kilka kluczowych przykładów języka Dart, które byłyby nowością dla programistów C# lub Java, które podsumujemy na końcu w pliku PDF w języku Dart.
Uwaga: ten artykuł dotyczy tylko Dart 2.x. Wersja 1.x nie była „w pełni gotowana” - w szczególności system typów był doradczy (jak TypeScript), a nie wymagany (jak C# lub Java).
1. Organizacja kodu
Najpierw omówimy jedną z najważniejszych różnic: sposób organizacji plików kodu i odwoływania się do nich.
Pliki źródłowe, zakres, przestrzenie nazw i importy
W języku C# kolekcja klas jest kompilowana do zestawu. Każda klasa ma przestrzeń nazw, a często przestrzenie nazw odzwierciedlają organizację kodu źródłowego w systemie plików — ale ostatecznie zespół nie zachowuje żadnych informacji o lokalizacji pliku kodu źródłowego.
W Javie pliki źródłowe są częścią pakietu, a przestrzenie nazw zwykle są zgodne z lokalizacją systemu plików, ale ostatecznie pakiet jest tylko zbiorem klas.
Tak więc oba języki mają sposób na utrzymanie kodu źródłowego nieco niezależnego od systemu plików.
Natomiast w języku Dart każdy plik źródłowy musi importować wszystko, do czego się odnosi, w tym inne pliki źródłowe i pakiety innych firm. Nie ma przestrzeni nazw w ten sam sposób i często odwołujesz się do plików za pośrednictwem ich lokalizacji w systemie plików. Zmienne i funkcje mogą być najwyższego poziomu, a nie tylko klasami. Pod tym względem Dart jest bardziej podobny do scenariusza.
Musisz więc zmienić sposób myślenia z „kolekcji klas” na coś w rodzaju „sekwencji dołączonych plików kodu”.
Dart obsługuje zarówno organizację pakietów, jak i organizację ad-hoc bez pakietów. Zacznijmy od przykładu bez pakietów, aby zilustrować kolejność dołączanych plików:
// file1.dart int alice = 1; // top level variable int barry() => 2; // top level function var student = Charlie(); // top level variable; Charlie is declared below but that's OK class Charlie { ... } // top level class // alice = 2; // top level statement not allowed // file2.dart import 'file1.dart'; // causes all of file1 to be in scope main() { print(alice); // 1 }
Wszystko, do czego odwołujesz się w pliku źródłowym, musi być zadeklarowane lub zaimportowane w tym pliku, ponieważ nie ma poziomu „projektu” ani innego sposobu włączenia innych elementów źródłowych do zakresu.
Jedynym zastosowaniem przestrzeni nazw w Dart jest nadanie importom nazwy, co wpływa na sposób odwoływania się do importowanego kodu z tego pliku.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Pakiety
Powyższe przykłady organizują kod bez pakietów. Aby korzystać z pakietów, kod jest organizowany w bardziej specyficzny sposób. Oto przykładowy układ pakietu dla pakietu o nazwie apples
:
-
apples/
-
pubspec.yaml
— definiuje nazwę pakietu, zależności i kilka innych rzeczy -
lib/
-
apples.dart
— import i eksport; to jest plik importowany przez wszystkich użytkowników pakietu -
src/
-
seeds.dart
— cały inny kod tutaj
-
-
-
bin/
-
runapples.dart
— zawiera główną funkcję, która jest punktem wejścia (jeśli jest to uruchamialny pakiet lub zawiera uruchamialne narzędzia)
-
-
Następnie możesz importować całe pakiety zamiast pojedynczych plików:
import 'package:apples';
Aplikacje nietrywialne zawsze powinny być zorganizowane w pakiety. Eliminuje to konieczność powtarzania ścieżek systemu plików w każdym pliku odsyłającym; plus, działają szybciej. Ułatwia również udostępnianie pakietu na pub.dev, gdzie inni programiści mogą go bardzo łatwo pobrać na własny użytek. Pakiety używane przez Twoją aplikację spowodują skopiowanie kodu źródłowego do systemu plików, dzięki czemu możesz debugować tak głęboko, jak chcesz.
2. Typy danych
Istnieją poważne różnice w systemie typów Darta, o których należy pamiętać, dotyczące wartości null, typów liczbowych, kolekcji i typów dynamicznych.
Nulls wszędzie
Pochodząc z C# lub Javy, jesteśmy przyzwyczajeni do typów pierwotnych lub wartościowych w odróżnieniu od typów referencyjnych lub obiektowych . Typy wartości są w praktyce alokowane na stosie lub w rejestrach, a kopie wartości są przesyłane jako parametry funkcji. Typy referencyjne są zamiast tego przydzielane na stercie, a tylko wskaźniki do obiektu są wysyłane jako parametry funkcji. Ponieważ typy wartości zawsze zajmują pamięć, zmienna o typie wartości nie może mieć wartości null, a wszystkie elementy członkowskie typu wartości muszą mieć zainicjowane wartości.
Dart eliminuje to rozróżnienie, ponieważ wszystko jest przedmiotem; wszystkie typy ostatecznie pochodzą od typu Object
. A więc jest to legalne:
int i = null;
W rzeczywistości wszystkie prymitywy są niejawnie inicjowane do null
. Oznacza to, że nie można zakładać, że domyślne wartości liczb całkowitych wynoszą zero, jak przywykłeś do C# lub Java, i może być konieczne dodanie sprawdzania wartości null.
Co ciekawe, nawet Null
jest typem, a słowo null
odnosi się do instancji Null
:
print(null.runtimeType); // prints Null
Nie tak wiele typów liczbowych
W przeciwieństwie do znanego asortymentu typów liczb całkowitych od 8 do 64 bitów ze znakami ze znakiem i bez znaku, głównym typem liczb całkowitych Darta jest po prostu int
, wartość 64-bitowa. (Istnieje również BigInt
dla bardzo dużych liczb.)
Ponieważ nie ma tablicy bajtów jako części składni języka, zawartość pliku binarnego może być przetwarzana jako listy liczb całkowitych, tj. List<Int>
.
Jeśli myślisz, że to musi być strasznie nieefektywne, projektanci już o tym pomyśleli. W praktyce istnieją różne reprezentacje wewnętrzne w zależności od rzeczywistej wartości całkowitej używanej w czasie wykonywania. Środowisko wykonawcze nie alokuje pamięci sterty dla obiektu int
, jeśli może to zoptymalizować i użyć rejestru procesora w trybie bez pudełka. Ponadto biblioteka byte_data
oferuje UInt8List
i kilka innych zoptymalizowanych reprezentacji.
Kolekcje
Kolekcje i generyki są bardzo podobne do tego, do czego jesteśmy przyzwyczajeni. Najważniejszą rzeczą, na którą należy zwrócić uwagę, jest to, że nie ma tablic o stałym rozmiarze: po prostu używaj typu danych List
wszędzie tam, gdzie chcesz użyć tablicy.
Dostępna jest również obsługa składni dla inicjowania trzech typów kolekcji:
final a = [1, 2, 3]; // inferred type is List<int>, an array-like ordered collection final b = {1, 2, 3}; // inferred type is Set<int>, an unordered collection final c = {'a': 1, 'b': 2}; // inferred type is Map<string, int>, an unordered collection of name-value pairs
Tak więc użyj List
Dart, w której użyjesz tablicy Java, ArrayList
lub Vector
; lub tablica C# lub List
. Użyj Set
, gdzie użyjesz Java/C# HashSet
. Użyj Map
, gdy użyjesz Dictionary
Java HashMap
lub C# .
3. Pisanie dynamiczne i statyczne
W językach dynamicznych, takich jak JavaScript, Ruby i Python, możesz odwoływać się do członków, nawet jeśli nie istnieją. Oto przykład JavaScript:
var person = {}; // create an empty object person.name = 'alice'; // add a member to the object if (person.age < 21) { // refer to a property that is not in the object // ... }
Jeśli to uruchomisz, person.age
będzie undefined
, ale i tak będzie działać.
Podobnie możesz zmienić typ zmiennej w JavaScript:
var a = 1; // a is a number a = 'one'; // a is now a string
Natomiast w Javie nie można napisać kodu takiego jak powyżej, ponieważ kompilator musi znać typ i sprawdza, czy wszystkie operacje są dozwolone — nawet jeśli użyjesz słowa kluczowego var:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java pozwala na kodowanie tylko z typami statycznymi. (Możesz użyć introspekcji, aby wykonać pewne dynamiczne zachowanie, ale nie jest to bezpośrednio częścią składni.) JavaScript i niektóre inne czysto dynamiczne języki pozwalają tylko na kodowanie z typami dynamicznymi.
Język Dart umożliwia zarówno:
// dart dynamic a = 1; // a is an int - dynamic typing a = 'one'; // a is now a string a.foo(); // we can call a function on a dynamic object, to be resolved at run time var b = 1; // b is an int - static typing // b = 'one'; // not allowed in Dart
Dart ma pseudo-typ dynamic
, który powoduje, że cała logika typów jest obsługiwana w czasie wykonywania. Próba wywołania a.foo()
nie zaszkodzi statycznemu analizatorowi i kod zostanie uruchomiony, ale nie powiedzie się w czasie wykonywania, ponieważ nie ma takiej metody.
C# był pierwotnie podobny do Java, a później dodał obsługę dynamiczną, więc Dart i C# są pod tym względem mniej więcej takie same.

4. Funkcje
Składnia deklaracji funkcji
Składnia funkcji w Dart jest nieco lżejsza i przyjemniejsza niż w C# lub Javie. Składnia jest dowolna z tych:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Na przykład:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Przekazywanie parametrów
Ponieważ wszystko jest obiektem, łącznie z prymitywami takimi jak int
i String
, przekazywanie parametrów może być mylące. Chociaż nie ma przekazywania parametru ref
, jak w C#, wszystko jest przekazywane przez referencję, a funkcja nie może zmienić referencji wywołującego. Ponieważ obiekty nie są klonowane po przekazaniu do funkcji, funkcja może zmienić właściwości obiektu. Jednak to rozróżnienie dla prymitywów, takich jak int i String, jest skutecznie dyskusyjne, ponieważ te typy są niezmienne.
var id = 1; var name = 'alice'; var client = Client(); void foo(int id, String name, Client client) { id = 2; // local var points to different int instance name = 'bob'; // local var points to different String instance client.State = 'AK'; // property of caller's object is changed } foo(id, name, client); // id == 1, name == 'alice', client.State == 'AK'
Parametry opcjonalne
Jeśli jesteś w świecie C# lub Java, prawdopodobnie przekląłeś w sytuacjach z myląco przeładowanymi metodami, takimi jak te:
// java void foo(string arg1) {...} void foo(int arg1, string arg2) {...} void foo(string arg1, Client arg2) {...} // call site: foo(clientId, input3); // confusing! too easy to misread which overload it is calling
Lub z opcjonalnymi parametrami C#, jest inny rodzaj zamieszania:
// c# void Foo(string arg1, int arg2 = 0) {...} void Foo(string arg1, int arg3 = 0, int arg2 = 0) {...} // call site: Foo("alice", 7); // legal but confusing! too easy to misread which overload it is calling and which parameter binds to argument 7 Foo("alice", arg2: 9); // better
C# nie wymaga nazywania opcjonalnych argumentów w witrynach wywołań, więc refaktoryzacja metod z opcjonalnymi parametrami może być niebezpieczna. Jeśli niektóre witryny wywołań są legalne po refaktoryzacji, kompilator ich nie przechwyci.
Dart ma bezpieczniejszy i bardzo elastyczny sposób. Przede wszystkim nie są obsługiwane metody przeciążone. Zamiast tego istnieją dwa sposoby obsługi parametrów opcjonalnych:
// positional optional parameters void foo(string arg1, [int arg2 = 0, int arg3 = 0]) {...} // call site for positional optional parameters foo('alice'); // legal foo('alice', 12); // legal foo('alice', 12, 13); // legal // named optional parameters void bar(string arg1, {int arg2 = 0, int arg3 = 0}) {...} bar('alice'); // legal bar('alice', arg3: 12); // legal bar('alice', arg3: 12, arg2: 13); // legal; sequence can vary and names are required
Nie możesz używać obu stylów w tej samej deklaracji funkcji.
async
pozycja słowa kluczowego
C# ma mylącą pozycję dla swojego słowa kluczowego async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Oznacza to, że sygnatura funkcji jest asynchroniczna, ale tak naprawdę tylko implementacja funkcji jest asynchroniczna. Każdy z powyższych podpisów byłby prawidłową implementacją tego interfejsu:
interface ICanFoo { Task<int> Foo(); }
W języku Dart async
znajduje się w bardziej logicznym miejscu, co oznacza, że implementacja jest asynchroniczna:
Future<int> foo() async {...}
Zakres i zamknięcia
Podobnie jak C# i Java, Dart ma zakres leksykalny. Oznacza to, że zmienna zadeklarowana w bloku wychodzi poza zakres na końcu bloku. Tak więc Dart obsługuje zamknięcia w ten sam sposób.
Składnia właściwości
Java spopularyzowała wzorzec get/set właściwości, ale język nie ma dla niego żadnej specjalnej składni:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# ma na to składnię:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart ma nieco inne właściwości wspierające składnię:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Konstruktorzy
Konstruktory Dart mają nieco większą elastyczność niż w C# czy Javie. Jedną z fajnych funkcji jest możliwość nazywania różnych konstruktorów w tej samej klasie:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Możesz wywołać domyślny konstruktor, używając tylko nazwy klasy: var c = Client();
Istnieją dwa rodzaje skrótów do inicjowania składowych instancji przed wywołaniem treści konstruktora:
class Client { String _code; String _name; Client(String this._name) // "this" shorthand for assigning parameter to instance member : _code = _name.toUpper() { // special out-of-body place for initializing // body } }
Konstruktorzy mogą uruchamiać konstruktory nadklas i przekierowywać do innych konstruktorów w tej samej klasie:
Foo.constructor1(int x) : this(x); // redirect to the default ctor in same class; no body allowed Foo.constructor2(int x) : super.plain(x) {...} // call base class named ctor, then run this body Foo.constructor3(int x) : _b = x + 1 : super.plain(x) {...} // initialize _b, then call base class ctor, then run this body
Konstruktory, które wywołują inne konstruktory w tej samej klasie w Javie i C#, mogą być mylące, gdy oba mają implementacje. W Dart ograniczenie polegające na tym, że konstruktory przekierowujące nie mogą mieć treści, zmusza programistę do wyraźniejszego wyświetlania warstw konstruktorów.
Istnieje również słowo kluczowe factory
, które pozwala na używanie funkcji jak konstruktora, ale implementacja jest po prostu zwykłą funkcją. Możesz go użyć, aby zwrócić instancję z pamięci podręcznej lub instancję typu pochodnego:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modyfikatory
W Javie i C# mamy modyfikatory dostępu, takie jak private
, protected
i public
. W Dart jest to drastycznie uproszczone: jeśli nazwa członka zaczyna się od podkreślenia, jest widoczna wszędzie wewnątrz pakietu (w tym dla innych klas) i niewidoczna dla rozmówców z zewnątrz; w przeciwnym razie jest widoczny zewsząd. Nie ma słów kluczowych, takich jak private
, które oznaczałyby widoczność.
Inny rodzaj modyfikatora kontroluje zmienność: słowa kluczowe final
i const
służą do tego celu, ale oznaczają różne rzeczy:
var a = 1; // a is variable, and can be reassigned later final b = a + 1; // b is a runtime constant, and can only be assigned once const c = 3; // c is a compile-time constant // const d = a + 2; // not allowed because a+2 cannot be resolved at compile time
7. Hierarchia klas
Język Dart obsługuje interfejsy, klasy i rodzaj dziedziczenia wielokrotnego. Jednak nie ma słowa kluczowego interface
; zamiast tego wszystkie klasy są również interfejsami, więc możesz zdefiniować klasę abstract
, a następnie ją zaimplementować:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
Dziedziczenie wielokrotne odbywa się za pomocą głównego rodowodu za pomocą słowa kluczowego extends
, a innych klas za pomocą słowa kluczowego with
:
class Employee extends Person with Salaried implements HasDesk {...}
W tej deklaracji klasa Employee
pochodzi od Person
i Salaried
, ale Person
to główna nadklasa, a Salaried
to mixin (dodatkowa nadklasa).
8. Operatorzy
Istnieje kilka zabawnych i przydatnych operatorów Dart, do których nie jesteśmy przyzwyczajeni.
Kaskady pozwalają na użycie wzoru łańcuchowego na wszystkim:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
Operator rozproszenia umożliwia traktowanie kolekcji jako listy jej elementów w inicjatorze:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Wątki
Dart nie ma wątków, co pozwala na transpilację do JavaScript. Zamiast tego ma „izolaty”, które bardziej przypominają oddzielne procesy, w tym sensie, że nie mogą dzielić pamięci. Ponieważ programowanie wielowątkowe jest tak podatne na błędy, to bezpieczeństwo jest postrzegane jako jedna z zalet Darta. Aby komunikować się między izolatami, musisz przesyłać strumieniowo dane między nimi; odebrane obiekty są kopiowane do przestrzeni pamięci izolatu odbierającego.
Rozwijaj się w języku darta: możesz to zrobić!
Jeśli jesteś programistą C# lub Java, to, co już wiesz, pomoże ci szybko nauczyć się języka Dart, ponieważ został zaprojektowany tak, aby był znajomy. W tym celu przygotowaliśmy ściągawkę w formacie PDF Dart w celach informacyjnych, skupiając się w szczególności na ważnych różnicach w stosunku do odpowiedników C# i Java:
Różnice pokazane w tym artykule w połączeniu z Twoją dotychczasową wiedzą pomogą Ci stać się produktywnym w ciągu pierwszego lub dwóch dni Dart. Udanego kodowania!