Bahasa Dart: Ketika Java dan C# Tidak Cukup Tajam

Diterbitkan: 2022-03-11

Jauh di tahun 2013, rilis resmi Dart 1.0 mendapat sorotan—seperti kebanyakan penawaran Google—tetapi tidak semua orang sekehendak tim internal Google untuk membuat aplikasi penting bisnis dengan bahasa Dart. Dengan pembangunan kembali Dart 2 yang dipikirkan dengan matang lima tahun kemudian, Google tampaknya telah membuktikan komitmennya terhadap bahasa tersebut. Memang, hari ini terus mendapatkan daya tarik di antara pengembang — terutama veteran Java dan C#.

Bahasa pemrograman Dart penting karena beberapa alasan:

  • Ini memiliki yang terbaik dari kedua dunia: Ini adalah bahasa yang dikompilasi, aman untuk tipe (seperti C # dan Java) dan bahasa skrip (seperti Python dan JavaScript) pada saat yang bersamaan.
  • Ini transpiles ke JavaScript untuk digunakan sebagai web front end.
  • Ini berjalan di semua hal, dan dikompilasi ke aplikasi seluler asli, sehingga Anda dapat menggunakannya untuk hampir semua hal.
  • Dart mirip dengan C# dan Java dalam sintaks, jadi cepat dipelajari.

Kita dari dunia C# atau Java dari sistem perusahaan yang lebih besar sudah tahu mengapa keamanan jenis, kesalahan waktu kompilasi, dan linter itu penting. Banyak dari kita ragu-ragu untuk mengadopsi bahasa "scripty" karena takut kehilangan semua struktur, kecepatan, akurasi, dan kemampuan debug yang biasa kita gunakan.

Tapi dengan pengembangan Dart, kita tidak harus menyerah semua itu. Kami dapat menulis aplikasi seluler, klien web, dan back end dalam bahasa yang sama—dan mendapatkan semua hal yang masih kami sukai tentang Java dan C#!

Untuk itu, mari kita telusuri beberapa contoh bahasa Dart utama yang baru bagi pengembang C# atau Java, yang akan kami rangkum dalam PDF bahasa Dart di bagian akhir.

Catatan: Artikel ini hanya mencakup Dart 2.x. Versi 1.x tidak "dimasak sepenuhnya" - khususnya, sistem tipe adalah penasihat (seperti TypeScript) alih-alih diperlukan (seperti C# atau Java).

1. Organisasi Kode

Pertama, kita akan masuk ke salah satu perbedaan paling signifikan: bagaimana file kode diatur dan direferensikan.

File Sumber, Cakupan, Ruang Nama, dan Impor

Dalam C#, kumpulan kelas dikompilasi menjadi sebuah Majelis. Setiap kelas memiliki ruang nama, dan sering kali ruang nama mencerminkan organisasi kode sumber dalam sistem file—tetapi pada akhirnya, majelis tidak menyimpan informasi apa pun tentang lokasi file kode sumber.

Di Java, file sumber adalah bagian dari sebuah paket dan ruang nama biasanya sesuai dengan lokasi sistem file, tetapi pada akhirnya, sebuah paket hanyalah kumpulan kelas.

Jadi kedua bahasa memiliki cara untuk menjaga kode sumber agak independen dari sistem file.

Sebaliknya, dalam bahasa Dart, setiap file sumber harus mengimpor semua yang dirujuknya, termasuk file sumber Anda yang lain dan paket pihak ketiga. Tidak ada ruang nama dengan cara yang sama, dan Anda sering merujuk ke file melalui lokasi sistem file mereka. Variabel dan fungsi bisa menjadi top-level, bukan hanya kelas. Dengan cara ini, Dart lebih seperti skrip.

Jadi, Anda perlu mengubah pemikiran Anda dari "kumpulan kelas" menjadi sesuatu yang lebih seperti "urutan file kode yang disertakan."

Dart mendukung organisasi paket dan organisasi ad-hoc tanpa paket. Mari kita mulai dengan contoh tanpa paket untuk mengilustrasikan urutan file yang disertakan:

 // 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 }

Semua yang Anda rujuk dalam file sumber harus dideklarasikan atau diimpor di dalam file itu, karena tidak ada level "proyek" dan tidak ada cara lain untuk menyertakan elemen sumber lain dalam cakupan.

Satu-satunya penggunaan ruang nama di Dart adalah memberi nama impor, dan itu memengaruhi cara Anda merujuk ke kode yang diimpor dari file itu.

 // file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }

Paket

Contoh di atas mengatur kode tanpa paket. Untuk menggunakan paket, kode diatur dengan cara yang lebih spesifik. Berikut ini contoh tata letak paket untuk paket bernama apples :

  • apples/
    • pubspec.yaml nama paket, dependensi, dan beberapa hal lainnya
    • lib/
      • apples.dart —impor dan ekspor; ini adalah file yang diimpor oleh konsumen paket mana pun
      • src/
        • seeds.dart —semua kode lainnya di sini
    • bin/
      • runapples.dart fungsi utama, yang merupakan titik masuk (jika ini adalah paket yang dapat dijalankan atau termasuk alat yang dapat dijalankan)

Kemudian Anda dapat mengimpor seluruh paket alih-alih file individual:

 import 'package:apples';

Aplikasi nontrivial harus selalu diatur sebagai paket. Ini mengurangi banyak keharusan untuk mengulang jalur sistem file di setiap file rujukan; ditambah, mereka berjalan lebih cepat. Ini juga memudahkan untuk membagikan paket Anda di pub.dev, di mana pengembang lain dapat dengan mudah mengambilnya untuk digunakan sendiri. Paket yang digunakan oleh aplikasi Anda akan menyebabkan kode sumber disalin ke sistem file Anda, sehingga Anda dapat men-debug paket tersebut sedalam yang Anda inginkan.

2. Tipe Data

Ada perbedaan utama dalam sistem tipe Dart yang harus diperhatikan, mengenai nulls, tipe numerik, koleksi, dan tipe dinamis.

nol di mana-mana

Berasal dari C# atau Java, kita terbiasa dengan tipe primitif atau nilai yang berbeda dari tipe referensi atau objek . Jenis nilai, dalam praktiknya, dialokasikan pada tumpukan atau register, dan salinan nilai dikirim sebagai parameter fungsi. Jenis referensi malah dialokasikan di heap, dan hanya pointer ke objek yang dikirim sebagai parameter fungsi. Karena tipe nilai selalu menempati memori, variabel tipe nilai tidak boleh nol, dan semua anggota tipe nilai harus memiliki nilai yang diinisialisasi.

Dart menghilangkan perbedaan itu karena semuanya adalah objek; semua tipe pada akhirnya berasal dari tipe Object . Jadi, ini sah:

 int i = null;

Faktanya, semua primitif secara implisit diinisialisasi ke null . Ini berarti Anda tidak dapat berasumsi bahwa nilai default bilangan bulat adalah nol seperti yang biasa Anda lakukan di C# atau Java, dan Anda mungkin perlu menambahkan pemeriksaan nol.

Menariknya, bahkan Null adalah sebuah tipe, dan kata null mengacu pada turunan dari Null :

 print(null.runtimeType); // prints Null

Tidak Banyak Jenis Numerik

Tidak seperti berbagai tipe integer yang familiar dari 8 hingga 64 bit dengan rasa yang ditandatangani dan tidak, tipe integer utama Dart hanyalah int , nilai 64-bit. (Ada juga BigInt untuk jumlah yang sangat besar.)

Karena tidak ada array byte sebagai bagian dari sintaks bahasa, konten file biner dapat diproses sebagai daftar bilangan bulat, yaitu List<Int> .

Jika Anda berpikir ini pasti sangat tidak efisien, para desainer sudah memikirkannya. Dalam praktiknya, ada representasi internal yang berbeda tergantung pada nilai integer aktual yang digunakan saat runtime. Runtime tidak mengalokasikan memori heap untuk objek int jika dapat mengoptimalkannya dan menggunakan register CPU dalam mode tanpa kotak. Juga, perpustakaan byte_data menawarkan UInt8List dan beberapa representasi lain yang dioptimalkan.

Koleksi

Koleksi dan obat generik sangat mirip dengan yang biasa kita gunakan. Hal utama yang perlu diperhatikan adalah bahwa tidak ada array berukuran tetap: Cukup gunakan tipe data List di mana pun Anda akan menggunakan array.

Juga, ada dukungan sintaksis untuk menginisialisasi tiga jenis koleksi:

 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

Jadi, gunakan Dart List di mana Anda akan menggunakan array Java, ArrayList , atau Vector ; atau array C# atau List . Gunakan Set di mana Anda akan menggunakan Java/C# HashSet . Gunakan Map di mana Anda akan menggunakan Java HashMap atau C# Dictionary .

3. Pengetikan Dinamis dan Statis

Dalam bahasa dinamis seperti JavaScript, Ruby, dan Python, Anda dapat mereferensikan anggota meskipun mereka tidak ada. Berikut ini contoh 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 // ... }

Jika Anda menjalankan ini, person.age akan menjadi undefined , tetapi tetap berjalan.

Demikian juga, Anda dapat mengubah jenis variabel dalam JavaScript:

 var a = 1; // a is a number a = 'one'; // a is now a string

Sebaliknya, di Java, Anda tidak dapat menulis kode seperti di atas karena kompilator perlu mengetahui jenisnya, dan memeriksa apakah semua operasi legal—bahkan jika Anda menggunakan kata kunci var:

 var b = 1; // a is an int // b = "one"; // not allowed in Java

Java hanya memungkinkan Anda untuk membuat kode dengan tipe statis. (Anda dapat menggunakan introspeksi untuk melakukan beberapa perilaku dinamis, tetapi itu bukan bagian langsung dari sintaks.) JavaScript dan beberapa bahasa dinamis murni lainnya hanya memungkinkan Anda untuk membuat kode dengan tipe dinamis.

Bahasa Dart memungkinkan keduanya:

 // 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 memiliki dynamic tipe semu yang menyebabkan semua logika tipe ditangani saat runtime. Upaya untuk memanggil a.foo() tidak akan mengganggu penganalisis statis dan kode akan berjalan, tetapi akan gagal saat runtime karena tidak ada metode seperti itu.

C# awalnya seperti Java, dan kemudian menambahkan dukungan dinamis, jadi Dart dan C# hampir sama dalam hal ini.

4. Fungsi

Sintaks Deklarasi Fungsi

Sintaks fungsi di Dart sedikit lebih ringan dan lebih menyenangkan daripada di C# atau Java. Sintaksnya adalah salah satu dari ini:

 // functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression

Sebagai contoh:

 void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';

Melewati Parameter

Karena semuanya adalah objek, termasuk primitif seperti int dan String , melewatkan parameter mungkin membingungkan. Meskipun tidak ada parameter ref yang lewat seperti di C#, semuanya dilewatkan dengan referensi, dan fungsi tidak dapat mengubah referensi pemanggil. Karena objek tidak dikloning saat diteruskan ke fungsi, fungsi dapat mengubah properti objek. Namun, perbedaan untuk primitif seperti int dan String secara efektif diperdebatkan karena tipe tersebut tidak dapat diubah.

 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'

Parameter Opsional

Jika Anda berada di dunia C# atau Java, Anda mungkin mengutuk situasi dengan metode kelebihan beban yang membingungkan seperti ini:

 // 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

Atau dengan parameter opsional C#, ada jenis kebingungan lain:

 // 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# tidak memerlukan penamaan argumen opsional di situs panggilan, jadi metode refactoring dengan parameter opsional bisa berbahaya. Jika beberapa situs panggilan menjadi legal setelah refactor, kompiler tidak akan menangkapnya.

Dart memiliki cara yang lebih aman dan sangat fleksibel. Pertama-tama, metode kelebihan beban tidak didukung. Sebagai gantinya, ada dua cara untuk menangani parameter opsional:

 // 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

Anda tidak dapat menggunakan kedua gaya dalam deklarasi fungsi yang sama.

Posisi Kata Kunci async

C# memiliki posisi yang membingungkan untuk kata kunci async -nya:

 Task<int> Foo() {...} async Task<int> Foo() {...}

Ini menyiratkan tanda tangan fungsi tidak sinkron, tetapi sebenarnya hanya implementasi fungsi yang tidak sinkron. Salah satu dari tanda tangan di atas akan menjadi implementasi yang valid dari antarmuka ini:

 interface ICanFoo { Task<int> Foo(); }

Dalam bahasa Dart, async berada di tempat yang lebih logis, yang menunjukkan implementasinya tidak sinkron:

 Future<int> foo() async {...}

Cakupan dan Penutupan

Seperti C# dan Java, Dart memiliki cakupan leksikal. Ini berarti variabel yang dideklarasikan dalam blok keluar dari ruang lingkup di akhir blok. Jadi Dart menangani penutupan dengan cara yang sama.

Sintaks properti

Java mempopulerkan pola get/set properti tetapi bahasanya tidak memiliki sintaks khusus untuk itu:

 // java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }

C# memiliki sintaks untuk itu:

 // c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }

Dart memiliki properti pendukung sintaks yang sedikit berbeda:

 // dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }

5. Konstruktor

Konstruktor Dart memiliki sedikit lebih banyak fleksibilitas daripada di C# atau Java. Salah satu fitur yang bagus adalah kemampuan untuk memberi nama konstruktor yang berbeda di kelas yang sama:

 class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }

Anda dapat memanggil konstruktor default hanya dengan nama kelas: var c = Client();

Ada dua jenis singkatan untuk menginisialisasi anggota instance sebelum badan konstruktor dipanggil:

 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 } }

Konstruktor dapat menjalankan konstruktor superclass dan mengarahkan ulang ke konstruktor lain di kelas yang sama:

 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

Konstruktor yang memanggil konstruktor lain di kelas yang sama di Java dan C# bisa membingungkan ketika keduanya memiliki implementasi. Di Dart, batasan yang mengarahkan konstruktor tidak dapat memiliki tubuh memaksa programmer untuk membuat lapisan konstruktor lebih jelas.

Ada juga kata kunci factory yang memungkinkan fungsi digunakan seperti konstruktor, tetapi implementasinya hanyalah fungsi biasa. Anda dapat menggunakannya untuk mengembalikan instance yang di-cache atau instance dari tipe turunan:

 class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);

6. Pengubah

Di Java dan C#, kami memiliki pengubah akses seperti private , protected , dan public . Di Dart, ini disederhanakan secara drastis: Jika nama anggota dimulai dengan garis bawah, itu terlihat di mana-mana di dalam paket (termasuk dari kelas lain) dan disembunyikan dari pemanggil luar; jika tidak, itu terlihat dari mana-mana. Tidak ada kata kunci seperti private untuk menandakan visibilitas.

Jenis pengubah lain mengontrol kemampuan berubah: Kata kunci final dan const adalah untuk tujuan itu, tetapi artinya berbeda:

 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. Hirarki Kelas

Bahasa Dart mendukung antarmuka, kelas, dan semacam pewarisan berganda. Namun, tidak ada kata kunci interface ; sebagai gantinya, semua kelas juga merupakan antarmuka, jadi Anda dapat mendefinisikan kelas abstract dan kemudian mengimplementasikannya:

 abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }

Warisan berganda dilakukan dengan garis keturunan utama menggunakan kata kunci extends , dan kelas lain menggunakan kata kunci with :

 class Employee extends Person with Salaried implements HasDesk {...}

Dalam deklarasi ini, class Employee diturunkan dari Person dan Salaried , tetapi Person adalah superclass utama dan Salaried adalah mixin (superclass sekunder).

8. Operator

Ada beberapa operator Dart yang menyenangkan dan berguna yang tidak biasa kami gunakan.

Kaskade memungkinkan Anda menggunakan pola rantai pada apa pun:

 emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();

Operator spread memungkinkan koleksi diperlakukan sebagai daftar elemennya di penginisialisasi:

 var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]

9. Benang

Dart tidak memiliki utas, yang memungkinkannya untuk diubah menjadi JavaScript. Ini memiliki "isolasi", yang lebih seperti proses terpisah, dalam arti bahwa mereka tidak dapat berbagi memori. Karena pemrograman multi-utas sangat rawan kesalahan, keamanan ini dipandang sebagai salah satu keunggulan Dart. Untuk berkomunikasi antar isolat, Anda perlu mengalirkan data di antara mereka; objek yang diterima disalin ke dalam ruang memori isolat penerima.

Kembangkan dengan Bahasa Dart: Anda Bisa Melakukan Ini!

Jika Anda seorang pengembang C# atau Java, apa yang sudah Anda ketahui akan membantu Anda mempelajari bahasa Dart dengan cepat, karena bahasa itu dirancang agar familiar. Untuk itu, kami telah mengumpulkan PDF lembar contekan Dart untuk referensi Anda, yang secara khusus berfokus pada perbedaan penting dari C# dan Java yang setara:

Lembar contekan bahasa Dart PDF

Perbedaan yang ditampilkan dalam artikel ini dikombinasikan dengan pengetahuan Anda yang ada akan membantu Anda menjadi produktif dalam satu atau dua hari pertama Dart. Selamat mengkode!

Terkait: Tenaga Hibrida: Keuntungan dan Manfaat Flutter