Dart 언어: Java와 C#이 충분히 날카롭지 않을 때
게시 됨: 2022-03-112013년으로 돌아가서 Dart의 공식 1.0 릴리스는 대부분의 Google 제품과 마찬가지로 일부 언론에 보도되었지만 모든 사람이 Google의 내부 팀만큼 Dart 언어로 비즈니스 크리티컬 앱을 만드는 데 열심인 것은 아닙니다. 5년 후 신중하게 설계된 Dart 2 재구축을 통해 Google은 언어에 대한 헌신을 입증한 것 같았습니다. 실제로 오늘날에도 개발자, 특히 Java 및 C# 베테랑 사이에서 계속해서 주목을 받고 있습니다.
Dart 프로그래밍 언어는 다음과 같은 몇 가지 이유로 중요합니다.
- 그것은 두 가지 장점을 모두 가지고 있습니다. 컴파일되고 유형이 안전한 언어(C# 및 Java와 같은)와 스크립팅 언어(Python 및 JavaScript와 같은)를 동시에 사용합니다.
- 웹 프런트 엔드로 사용하기 위해 JavaScript로 변환됩니다.
- 모든 것에서 실행되고 기본 모바일 앱으로 컴파일되므로 거의 모든 것에 사용할 수 있습니다.
- Dart는 구문 면에서 C# 및 Java와 유사하므로 빠르게 배울 수 있습니다.
대규모 엔터프라이즈 시스템의 C# 또는 Java 세계에서 우리 중 누군가는 이미 형식 안전성, 컴파일 시간 오류 및 린터가 중요한 이유를 알고 있습니다. 우리 중 많은 사람들이 우리에게 익숙한 모든 구조, 속도, 정확성 및 디버그 가능성을 잃을까 두려워 "스크립트" 언어를 채택하는 것을 주저합니다.
그러나 Dart 개발을 통해 우리는 그 중 어느 것도 포기할 필요가 없습니다. 모바일 앱, 웹 클라이언트 및 백엔드를 동일한 언어로 작성할 수 있으며 Java 및 C#에 대해 여전히 좋아하는 모든 것을 얻을 수 있습니다!
이를 위해 C# 또는 Java 개발자에게 생소할 몇 가지 주요 Dart 언어 예제를 살펴보겠습니다. 이 예제는 마지막에 Dart 언어 PDF로 요약됩니다.
참고: 이 문서에서는 Dart 2.x만 다룹니다. 버전 1.x는 "완전히 요리"되지 않았습니다. 특히 유형 시스템은 필수(C# 또는 Java와 같은)가 아니라 권고(TypeScript와 같은)였습니다.
1. 코드 구성
먼저 가장 중요한 차이점 중 하나인 코드 파일이 구성되고 참조되는 방식에 대해 알아보겠습니다.
소스 파일, 범위, 네임스페이스 및 가져오기
C#에서 클래스 컬렉션은 어셈블리로 컴파일됩니다. 각 클래스에는 네임스페이스가 있으며 종종 네임스페이스는 파일 시스템의 소스 코드 구성을 반영하지만 결국 어셈블리는 소스 코드 파일 위치에 대한 정보를 유지하지 않습니다.
Java에서 소스 파일은 패키지의 일부이고 네임스페이스는 일반적으로 파일 시스템 위치를 따르지만 결국 패키지는 클래스의 모음일 뿐입니다.
따라서 두 언어 모두 소스 코드를 파일 시스템과 어느 정도 독립적으로 유지하는 방법이 있습니다.
대조적으로, Dart 언어에서 각 소스 파일은 다른 소스 파일 및 타사 패키지를 포함하여 참조하는 모든 것을 가져와야 합니다. 동일한 방식으로 네임스페이스가 없으며 파일 시스템 위치를 통해 파일을 참조하는 경우가 많습니다. 변수와 함수는 클래스뿐만 아니라 최상위 수준이 될 수 있습니다. 이런 면에서 Dart는 스크립트와 비슷합니다.
따라서 "클래스 모음"에서 "포함된 코드 파일 시퀀스"와 같은 것으로 생각을 바꿔야 합니다.
Dart는 패키지 구성과 패키지가 없는 임시 구성을 모두 지원합니다. 포함된 파일의 순서를 설명하기 위해 패키지가 없는 예제부터 시작하겠습니다.
// 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 }
소스 파일에서 참조하는 모든 것은 해당 파일 내에서 선언하거나 가져와야 합니다. "프로젝트" 수준이 없고 범위에 다른 소스 요소를 포함할 수 있는 다른 방법이 없기 때문입니다.
Dart에서 네임스페이스의 유일한 용도는 가져오기에 이름을 지정하는 것이며 이는 해당 파일에서 가져온 코드를 참조하는 방법에 영향을 미칩니다.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
패키지
위의 예는 패키지 없이 코드를 구성합니다. 패키지를 사용하기 위해 코드는 보다 구체적인 방식으로 구성됩니다. 다음은 apples
이라는 패키지에 대한 패키지 레이아웃의 예입니다.
-
apples/
-
pubspec.yaml
— 패키지 이름, 종속성 및 기타 사항을 정의합니다. -
lib/
-
apples.dart
—수입 및 수출; 이것은 패키지의 모든 소비자가 가져온 파일입니다. -
src/
-
seeds.dart
- 여기에 있는 다른 모든 코드
-
-
-
bin/
-
runapples.dart
- 진입점인 주 기능을 포함합니다(실행 가능한 패키지이거나 실행 가능한 도구가 포함된 경우)
-
-
그런 다음 개별 파일 대신 전체 패키지를 가져올 수 있습니다.
import 'package:apples';
중요하지 않은 응용 프로그램은 항상 패키지로 구성해야 합니다. 이렇게 하면 각 참조 파일에서 파일 시스템 경로를 반복해야 하는 번거로움이 줄어듭니다. 게다가 더 빨리 달린다. 또한 pub.dev에서 패키지를 쉽게 공유할 수 있으므로 다른 개발자가 자신의 용도로 매우 쉽게 가져올 수 있습니다. 앱에서 사용하는 패키지로 인해 소스 코드가 파일 시스템에 복사되므로 원하는 만큼 해당 패키지에 대해 디버그할 수 있습니다.
2. 데이터 유형
Dart의 유형 시스템에는 null, 숫자 유형, 컬렉션 및 동적 유형과 관련하여 알아야 할 주요 차이점이 있습니다.
모든 곳에서 Null
C# 또는 Java에서 나온 우리는 참조 또는 개체 유형과 구별되는 기본 또는 값 유형에 익숙합니다. 값 유형은 실제로 스택이나 레지스터에 할당되며 값의 복사본이 함수 매개변수로 전송됩니다. 참조 유형은 대신 힙에 할당되고 객체에 대한 포인터만 함수 매개변수로 전송됩니다. 값 형식은 항상 메모리를 차지하므로 값 형식 변수는 null일 수 없으며 모든 값 형식 멤버에는 초기화된 값이 있어야 합니다.
Dart는 모든 것이 객체이기 때문에 이러한 구분을 제거합니다. 모든 유형은 궁극적으로 Object
유형에서 파생됩니다. 따라서 이것은 합법적입니다.
int i = null;
사실, 모든 프리미티브는 암시적으로 null
로 초기화됩니다. 즉, C# 또는 Java에서 사용하던 것처럼 정수의 기본값이 0이라고 가정할 수 없으며 null 검사를 추가해야 할 수도 있습니다.
흥미롭게도 Null
도 유형이고 null
이라는 단어는 Null
의 인스턴스를 나타냅니다.
print(null.runtimeType); // prints Null
숫자 유형이 많지 않음
8~64비트 정수 유형과 부호 있는 유형 및 부호 없는 유형의 정수 유형과 달리 Dart의 주요 정수 유형은 64비트 값인 int
입니다. (매우 큰 숫자에 대한 BigInt
도 있습니다.)
언어 구문의 일부로 바이트 배열이 없기 때문에 이진 파일 내용은 정수 목록(예: List<Int>
)으로 처리될 수 있습니다.
이것이 끔찍하게 비효율적이라고 생각한다면 디자이너는 이미 그것에 대해 생각했습니다. 실제로 런타임에 사용되는 실제 정수 값에 따라 내부 표현이 다릅니다. 런타임은 int 개체를 최적화하고 unboxed 모드에서 CPU 레지스터를 사용할 수 있는 경우 int
개체에 힙 메모리를 할당하지 않습니다. 또한 라이브러리 byte_data
는 UInt8List
및 기타 최적화된 표현을 제공합니다.
컬렉션
컬렉션과 제네릭은 우리에게 익숙한 것과 매우 유사합니다. 주목해야 할 주요 사항은 고정 크기 배열이 없다는 것입니다. 배열을 사용할 때마다 List
데이터 유형을 사용하기만 하면 됩니다.
또한 다음 세 가지 컬렉션 유형을 초기화하기 위한 구문 지원이 있습니다.
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
따라서 Java 배열, ArrayList
또는 Vector
를 사용할 위치에서 Dart List
를 사용하십시오. 또는 C# 배열 또는 List
. Java/C# HashSet
을 사용할 위치에 Set
을 사용하십시오. Java HashMap
또는 C# Dictionary
을 사용하는 경우 Map
을 사용합니다.
3. 동적 및 정적 타이핑
JavaScript, Ruby 및 Python과 같은 동적 언어에서는 구성원이 존재하지 않더라도 참조할 수 있습니다. 다음은 자바스크립트 예제입니다.
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 // ... }
이것을 실행하면 person.age
가 undefined
하지만 어쨌든 실행됩니다.
마찬가지로 JavaScript에서 변수 유형을 변경할 수 있습니다.
var a = 1; // a is a number a = 'one'; // a is now a string
대조적으로 Java에서는 컴파일러가 유형을 알아야 하고 var 키워드를 사용하더라도 모든 작업이 올바른지 확인하기 때문에 위와 같은 코드를 작성할 수 없습니다.
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java에서는 정적 유형으로만 코딩할 수 있습니다. (인트로스펙션을 사용하여 일부 동적 동작을 수행할 수 있지만 구문의 직접적인 일부는 아닙니다.) JavaScript 및 기타 순수 동적 언어에서는 동적 유형으로만 코딩할 수 있습니다.
Dart 언어는 다음을 모두 허용합니다.
// 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에는 모든 유형 논리가 런타임에 처리되도록 하는 유사 유형 dynamic
이 있습니다. a.foo()
를 호출하려는 시도는 정적 분석기를 방해하지 않고 코드가 실행되지만 이러한 메서드가 없기 때문에 런타임에 실패합니다.
C#은 원래 Java와 같았고 나중에 동적 지원이 추가되었으므로 Dart와 C#은 이 점에서 거의 동일합니다.
4. 기능
함수 선언 구문
Dart의 함수 구문은 C# 또는 Java보다 약간 더 가볍고 재미있습니다. 구문은 다음 중 하나입니다.
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
예를 들어:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
매개변수 전달
int
및 String
과 같은 프리미티브를 포함하여 모든 것이 객체이기 때문에 매개변수 전달이 혼란스러울 수 있습니다. C#처럼 전달되는 ref
매개변수가 없지만 모든 것은 참조로 전달되며 함수는 호출자의 참조를 변경할 수 없습니다. 객체는 함수에 전달될 때 복제되지 않기 때문에 함수는 객체의 속성을 변경할 수 있습니다. 그러나 int 및 String과 같은 프리미티브에 대한 구분은 이러한 유형이 변경 불가능하기 때문에 사실상 무의미합니다.

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'
선택적 매개변수
C# 또는 Java 세계에 있다면 다음과 같이 혼란스럽게 오버로드된 메서드가 있는 상황에서 저주를 받았을 것입니다.
// 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
또는 C# 선택적 매개변수를 사용하면 다른 종류의 혼동이 있습니다.
// 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#은 호출 사이트에서 선택적 인수의 이름을 지정할 필요가 없으므로 선택적 매개변수가 있는 리팩토링 메서드는 위험할 수 있습니다. 리팩터링 후에 일부 호출 사이트가 합법적인 경우 컴파일러는 해당 사이트를 포착하지 않습니다.
Dart는 더 안전하고 매우 유연한 방법을 가지고 있습니다. 우선 오버로드된 메서드는 지원 되지 않습니다 . 대신 선택적 매개변수를 처리하는 두 가지 방법이 있습니다.
// 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
동일한 함수 선언에서 두 스타일을 모두 사용할 수는 없습니다.
async
키워드 위치
C#은 async
키워드에 대해 혼란스러운 위치를 가지고 있습니다.
Task<int> Foo() {...} async Task<int> Foo() {...}
이것은 함수 서명이 비동기적임을 의미하지만 실제로는 함수 구현만 비동기적입니다. 위의 서명 중 하나는 이 인터페이스의 유효한 구현입니다.
interface ICanFoo { Task<int> Foo(); }
Dart 언어에서 async
는 구현이 비동기적임을 나타내는 보다 논리적인 위치에 있습니다.
Future<int> foo() async {...}
범위 및 폐쇄
C# 및 Java와 마찬가지로 Dart는 어휘 범위가 지정됩니다. 이는 블록에서 선언된 변수가 블록 끝에서 범위를 벗어남을 의미합니다. 따라서 Dart는 클로저를 같은 방식으로 처리합니다.
속성 구문
Java는 속성 get/set 패턴을 대중화했지만 언어에는 이에 대한 특별한 구문이 없습니다.
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C#에는 다음과 같은 구문이 있습니다.
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart는 속성을 지원하는 약간 다른 구문을 가지고 있습니다.
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. 생성자
Dart 생성자는 C#이나 Java보다 훨씬 더 유연합니다. 한 가지 좋은 기능은 동일한 클래스에서 다른 생성자의 이름을 지정할 수 있다는 것입니다.
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
클래스 이름만으로 기본 생성자를 호출할 수 있습니다. var c = Client();
생성자 본문이 호출되기 전에 인스턴스 멤버를 초기화하기 위한 두 가지 종류의 약어가 있습니다.
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 } }
생성자는 슈퍼클래스 생성자를 실행하고 동일한 클래스의 다른 생성자로 리디렉션할 수 있습니다.
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
Java 및 C#에서 동일한 클래스의 다른 생성자를 호출하는 생성자는 둘 다 구현되어 있을 때 혼동될 수 있습니다. Dart에서 리디렉션 생성자가 본문을 가질 수 없다는 제한은 프로그래머가 생성자의 레이어를 더 명확하게 만들도록 강제합니다.
함수를 생성자처럼 사용할 수 있도록 하는 factory
키워드도 있지만 구현은 일반 함수일 뿐입니다. 이를 사용하여 캐시된 인스턴스 또는 파생된 유형의 인스턴스를 반환할 수 있습니다.
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. 수정자
Java 및 C#에는 private
, protected
및 public
과 같은 액세스 수정자가 있습니다. Dart에서 이것은 크게 단순화되었습니다. 멤버 이름이 밑줄로 시작하면 패키지 내부(다른 클래스 포함) 어디에서나 볼 수 있고 외부 호출자에게는 숨겨집니다. 그렇지 않으면 모든 곳에서 볼 수 있습니다. 가시성을 나타내는 private
와 같은 키워드는 없습니다.
변경성을 제어하는 또 다른 종류의 수정자: final
및 const
키워드는 이러한 목적을 위한 것이지만 의미가 다릅니다.
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. 클래스 계층
Dart 언어는 인터페이스, 클래스 및 일종의 다중 상속을 지원합니다. 그러나 interface
키워드는 없습니다. 대신 모든 클래스도 인터페이스이므로 abstract
클래스를 정의한 다음 구현할 수 있습니다.
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
다중 상속은 extends
키워드를 사용하는 기본 계보 with
키워드를 사용하는 다른 클래스에서 수행됩니다.
class Employee extends Person with Salaried implements HasDesk {...}
이 선언에서 Employee
클래스는 Person
및 Salaried
에서 파생되지만 Person
은 기본 수퍼 클래스이고 Salaried
는 mixin(보조 수퍼 클래스)입니다.
8. 운영자
익숙하지 않은 재미있고 유용한 Dart 연산자가 있습니다.
캐스케이드 를 사용하면 무엇이든 연결 패턴을 사용할 수 있습니다.
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
확산 연산자를 사용하면 컬렉션이 초기화 프로그램에서 해당 요소 목록으로 처리될 수 있습니다.
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. 스레드
Dart에는 스레드가 없으므로 JavaScript로 변환할 수 있습니다. 대신 메모리를 공유할 수 없다는 점에서 별도의 프로세스와 유사한 "격리"가 있습니다. 다중 스레드 프로그래밍은 오류가 발생하기 쉬우므로 이러한 안전성은 Dart의 장점 중 하나로 간주됩니다. 분리물 간에 통신하려면 분리물 간에 데이터를 스트리밍해야 합니다. 수신된 개체는 수신 격리의 메모리 공간으로 복사됩니다.
Dart 언어로 개발: 할 수 있습니다!
C# 또는 Java 개발자라면 이미 알고 있는 내용이 Dart 언어를 빠르게 배우는 데 도움이 될 것입니다. Dart는 친숙하도록 설계되었기 때문입니다. 이를 위해 참조용으로 Dart 치트 시트 PDF를 작성했으며 특히 C# 및 Java 동등물과의 중요한 차이점에 중점을 둡니다.
이 기사에 나와 있는 차이점과 기존 지식을 결합하면 Dart를 처음 사용하거나 이틀 만에 생산성을 높이는 데 도움이 됩니다. 즐거운 코딩!