Dart 배우기
Dart-00   Install Dart SDK
Dart-01   Codelabs
Dart-02   Language Tour
Dart-03   Type System
Dart-04   Effective Dart

Type System

Dart 언어는 type safe 언어이다: static type 검사와 runtime 검사의 조합을 사용하여, 변수의 값이 항상 sound typing이라고도 하는 변수의 static type과 항상 일치하는지 확인한다. types는 필수적이지만, type 추론 때문에 type annotations는 선택 사항이다.

static type 검사의 한 가지 이점은 Dart의 static analyzer를 사용하여 compile time에 bug를 찾을 수 있다는 것이다.

generic class에 type annotation을 추가하여 static analysis error를 수정할 수 있다. 가장 일반적인 generic class는 List<T>Map<K,V> 같은 collection type이다.

예를 들어, 다음 코드에서 printInts() 함수는 integer list를 출력하고, main()은 list를 만들어 printInts()에 전달한다.

// static analysis: error/warning
void printInt(List<int> a) => print(a);

void main() {
    final list = [];
    list.add(1);
    list.add('2');
    printInts(list);
}

앞의 코드는 printInts(list)를 호출할 때 list에서 type error를 발생시킨다.

error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable

이 error는 List<dynamic>에서 List<int>로의 unsound한 암시적 case를 강조한다. list 변수의 static type은 List<dynamic>이다. 이는 초기화 선언 ‘var list = []dynamic보다 더 구체적인 type argument를 추론하기에 충분한 정보를 analyzer에 제공하지 않기 때문이다. List<int> type의 parameter를 예상하므로, type이 일치하지 않는다.

list 생성 시, type annotation을 추가할 때, analyzer는 string argument를 int parameter에 할당할 수 없다고 불평한다. list.add('2') code에서 따옴표를 제거하면, static analysis를 통과하고 error나 warning 없이 실행되는 code가 생성된다.

// static analysis: success
void printInts(List<int> a) => print(a)

void main() {
    final list = <int>[];
    list.add(1);
    list.add(2);
    printInts(list);
}

1. What is soundness?

Soundness는 program이 특정 유효하지 않은 상태에 들어갈 수 없도록 하는 것이다. sound type system은 expression이 expression의 static type과 일치하지 않는 값으로 평가되는 상태에 절대 들어갈 수 없음을 의미한다. 예를 들어, expression의 static type이 String인 경우, runtime 시 평가할 때만 string을 얻을 수 있다.

Java 및 C#의 type system과 마찬가지로, Dart의 type system은 sound 하다. static 검사(compile-time errors)와 runtime 검사의 조합을 사용하여 soundness를 적용한다. 예를 들어, Stringint를 할당하는 것은 compile-time error이다. as String을 사용하여 ObjectString을 type casting 하는 것은, object가 String이 아니라면 runtime error와 함께 실패한다.

2. The benefits of soundness

sound type system은 다음과 같은 몇 가지 이점이 있다.

  • compile time에 type과 관련된 bug를 공개한다. - sound type system은 code가 해당 type에 대해 모호하지 않도록 강제하므로, runtime에 찾기 어려울 수 있는 type 관련 bug가 compile-time에 드러난다.

  • 더 읽기 쉬운 code - 실제로 지정된 type을 갖는 값에 의존할 수 있기 때문에, code를 읽기가 더 쉽다. sound dart에서 type은 거짓말을 할 수 없다.

  • 유지 보수가 더 쉬운 코드 - sound type system을 사용하면, 한 code 조각을 변경할 때, type system이 방금 깨진 다른 code 조각에 대해 경고할 수 있다.

  • AOT(Ahead of Time) compile이 더 좋다. - AOT compilation은 type 없이 가능하지만, 생성된 code는 훨씬 덜 효율적이다.

3. Tips for passing static analysis

static type에 대한 대부분의 규칙은 이해하기 쉽다. 다음은 덜 분명한 규칙 중 일부이다.

  • method를 overriding 할 때 sound return type을 사용한다.
  • method를 overriding 할 때, sound parameter type을 사용한다.
  • dynamic list를 typed list로 사용하지 않는다.

다음 type 계층 구조를 사용하는 예제와 함께, 이러한 규칙을 살펴보자.

Animal -> Aligator
$\;\;\;\;\;\;\;\;\;\;$-> HoneyBadger
$\;\;\;\;\;\;\;\;\;\;$-> Cat -> Lion
$\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;$-> MaineCoon

A. Use sound return types when overriding methods

subclass에 있는 method의 return type은 superclass에 있는 method의 return type과 동일한 type이거나 subtype이어야 한다. Animal class의 getter method를 고려한다:

class Animal {
    void chase(Animal a) { ... }
    Animal get parent => ...
}

parent getter method는 Animal을 return한다. HoneyBadger subclass에서, getter의 return type을 HoneyBadger(또는 Animal의 다른 subtype)로 바꿀 수 있지만, 관련 없는 type은 허용되지 않는다.

// static analysis: success
class HoneyBadger extends Animal {
    @override
    void chase(Animal a) { ... }

    @override
    HoneyBadger get parent => ...
}
// static analysis: error/warning
class HoneyBadger extends Animal {
    @override
    void chase(Animal a) { ... }

    @override
    Root get parent => ...
}

B. Use sound parameter types when overriding methods

override된 method의 parameter는 superclass에 있는 해당 parameter의 type 또는 supertype이 동일해야 한다. type을 원래 parameter의 subtype으로 교체하여 parameter type을 “tighten”하게 하지 않아야 한다.

subtype을 사용해야 하는 타당한 이유가 있는 경우, convariant keyword를 사용할 수 있다.

Animal class의 chase(Animal) method를 고려한다:

class Aniaml {
    void chase(Animal a) { ... }
    Animal get parent => ...
}

chase() method는 Animal을 갖는다. HoneyBadger는 무엇이든 chase 한다. 무엇이든(Object) chase 하기 위해서, chase() method를 override 해도 된다.

// static analysis: success
class HoneyBadger extends Animal {
    @override
    void chase(Object a) { ... }

    @override
    Animal get parent => ...
}

이 code는 cat을 정의하고 alligator 뒤에 보낼 수 있기 때문에, type이 안전하지 않다.

Animal a = Cat();
a.chase(Alligator());  // Not type safe or feline safe.

다음 code는 chase() method의 paramter를 Animal에서 Animal의 subclass인 Mouse로 tighten 한다.

// static analysis: error/warning
class Mouse extends Animal {...}

class Cat extends Animal {
    @override
    void chase(Mouse x) { ... }
}

C. Don’t use a dynamic list as a typed list

dynamic list는 다양한 종류의 list를 갖고 싶을 때 좋다. 그러나, dynamic list를 typed list로 사용할 수 없다.

이 규칙은 generic type의 instance에도 적용된다.

다음 code는 Dogdynamic list를 만들고, Cat type의 list에 이를 할당한다. 이는, static analysis에서 error를 일으킨다.

// static analysis: error/warning
class Cat extends Animal { ... }

class Dog extends Animal { ... }

void main() {
    List<Cat> foo = <dynamic>[Dog()];   // Error
    List<dynamic> bar = <dynamic>[Dog(), Cat()];    // OK
}

4. Runtime checks

Dart VM 및 dartdevc의 runtime 검사는 analyzer가 포착할 수 없는 type safety issue를 처리한다.

예를 들어, 다음 code는 dog list를 cat list로 casting 하는 것은 error이기 때문에, runtime에 error를 throw 한다.

// runtime: error
void main() {
    List<Animal> animals = [Dog()];
    List<Cat> cats = animals as List<Cat>;
}

5. Type inference

analyzer는 field, method, 지역 변수, generic type argument에 대한 type을 유추할 수 있다. analyzer에 특정 type을 유추할 수 있는 정보가 충분하지 않으면, dynamic type을 사용한다.

다음은 type 유추가 generic과 함께 작동하는 방식의 예이다. 이 예에서, argument라는 이름의 변수는 다양한 type의 value와 string key를 쌍으로 연결하는 map을 보유한다.

변수를 명시적으로 입력하면, 다음과 같이 작성할 수 있다.

Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};

대안으로, var 또는 final을 사용하여 Dart가 type을 추론하도록 할 수 있다.

var arguments = {'argA': 'hello', 'argB': 42};  // Map<String, Object>

map literal은 항목에서 type을 유추하고, 변수는 map literal의 type에서 type을 유추한다. 이 map에서 key는 둘 다 string이지만, 값은 서로 다른 type(Object라는 상한을 갖는 Stringint)을 갖는다. 따라서 map literal은 Map<String, Object>를 갖고, arguments 변수도 마찬가지이다.

A. Field and method inference

지정된 type이 없고 superclass의 field 또는 method를 override하는 field 또는 method는, superclass method 또는 field의 type을 상속한다.

선언되거나 상속된 type이 없지만 초기 값으로 선언된 field는, 초기 값을 기반으로 유추된 type을 가져온다.

B. Static field inference

static field와 변수는 initializer에서 유추된 type을 가져온다. cycle이 발생하면 추론이 실패한다. (즉, 변수의 type을 추론하는 것은 해당 변수의 type을 아는 것에 달려 있음.)

C. Local variable inference

지역 변수 type은 initializer program에서 추론된다(있는 경우). 후속 할당은 고려되지 않는다. 이는 너무 정확한 type이 유추될 수 있음을 의미할 수 있다. 그렇다면, type annotation을 추가할 수 있다.

// static analysis: error/warning
var x = 3;  // x is inferred as an int.
x = 4.0;
// static analysis: success
num y = 3;  // A num can be double or int.
y = 4.0;

D. Type argument inference

생성자 호출 및 generic method 호출에 대한 type argument는 발생 context의 하향 정보와 생성자 또는 generic method에 대한 argument의 상향 정보 조합을 기반으로 유추된다. 추론이 당신이 원하거나 기대하는 것을 하지 않는다면, 당신은 항상 명시적으로 type argument를 지정할 수 있다.

// static analysis: success
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];

// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];

// Inferred as Iterable<int>.
var ints = listOfDouble.map((x) => x.toInt());

마지막 예에서는, x는 햐향 정보를 사용하여 double로 유추된다. Dart는 map() method의 type argument인 <int>를 유추할 때, 이 return type을 상향 정보로 사용한다.

6. Substituting types

method를 override 할 때, 한 가지 type(old method에서)을 새로운 type(new method에서)을 가질 수 있는 것으로 교체한다. 마찬가지로, 함수에 argument를 전달할 때, 한 유형(선언된 유형의 parameter)이 있는 항목을 다른 type(실제 argument)이 있는 항목으로 대체한다. type이 하나인 것을 subtype이나 supertype이 있는 것으로 대체할 수 있는 경우는 언제인가?

type을 대체할 때, consumers와 producers의 관점에서 생각하는 것이 도움이 된다. consumer는 type을 흡수하고 producer는 type을 생성한다.

consumer’s type을 supertype으로 바꾸고 producer’s type을 subtype으로 바꿀 수 있다.

generic type을 사용한 simple type assignment를 살펴보자.

A. Simple type assignment

객체에 객체를 할당할 때, type을 다른 type으로 대체할 수 있는 경우는 언제인가? 대답은 객체가 consumer인지 producer인지에 따라 다르다.

다음 type 계층을 고려한다.

Animal -> Aligator
$\;\;\;\;\;\;\;\;\;\;$-> HoneyBadger
$\;\;\;\;\;\;\;\;\;\;$-> Cat -> Lion
$\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;$-> MaineCoon

Cat c는 consumer이고 Cat()는 producer인 다음과 같은 simple assignment를 고려한다:

Cat c = Cat();

consuming 위치에서, 특정 type(Cat)을 소비하는 무언가를 무엇이든 소비하는 무언가(Animal)로 바꾸는 것이 안전하므로, AnimalCat의 supertype이기 때문에, Cat cAnimal c로 대체하는 것이 허용된다.

// static analysis: success
Animal c = Cat();

그러나, superclass가 Lion과 같은 다른 동작을 가진 Cat type을 제공할 수 있기 때문에, Cat cMaineCoon c로 대체하는 것은 type safety를 거스른다.

// static analysis: error/warning
MaineCoon c = Cat();

producing 위치에서, type(Cat)을 생성하는 것을 보다 구체적인 유형(MaineCoon)으로 바꾸는 것이 안전하다. 따라서 다음이 허용된다:

// static analysis: success
Cat c = MaineCoon();

B. Generic type assignment

generic type에도 규칙이 동일한가? 그렇다. Cat ListAnimal List의 subtype이고, MaineCoon List의 supertype인 동물 list 계층 구조를 고려한다:

List<Animal> -> List<Cat> -> List<MaineCoon>

다음 예에서는, List<MaineCoon>List<Cat>의 subtype이기 때문에 MaineCoon list를 myCats에 할당할 수 있다.

// static analysis: success
List<Cat> myCats = <MaineCoon>[];

다른 방향으로 가면 어떠한가? Animal list를 List<Cat>에 할당할 수 있는가?

// static analysis: error/warning
List<Cat> myCats = <Animal>[];

이 assignment는 Animal과 같이 dynamic이 아닌 type을 허용하지 않는 암시적 downcast를 생성하기 때문에, static analysis를 통과하지 못한다.

2.12 이전의 language version을 사용하는 package (null safety가 도입된 지원)에서, code는 이러한 non-dynamic type에서 암시적으로 downcast 될 수 있다. analysis options file에서 implicit-casts를 지정하여, 2.12 이전 project에서 non-dynamic downcast를 허용하지 않을 수 있다.

이 code가 static analysis를 통과하도록 하려면, runtime에 실패할 수 있는 명시적 cast를 사용한다.

List<Cat> myCats = <Animal>[] as List<Cat>;

C. Methods

method를 override할 때, producer와 consumer 규칙이 계속 적용된다. 예를 들어:

class Animal {
    void chase(Animal a) {}     // chase: Consumer
    Animal get parent => ...    // get parent: Producer
}

consumer(chase(Animal))의 경우, parameter 유형을 supertype으로 바꿀 수 있다. producer(parent getter method)의 경우, return type을 subtype으로 바꿀 수 있다.

태그:

카테고리:

업데이트:

댓글남기기