[Dart] Dart 문법

12 분 소요

0. 변수 타입

var, int, number, String, bool


1. var vs dynamic

var타입은 선언했을 때 지정된 타입으로 고정된다.

void main() {
  var name = 'Kim';
  name = 1; // Error: A value of type 'int' can't be assigned to a variable of type 'String'.
}

dynamic 타입은 타입을 동적으로 바꿀 수 있다.


void main() {
  dynamic name = "kim";
  name = 1;
}

var로 선언만 하면, 그 밑에서는 다양한 타입 값으로 설정해도 에러가 발생하지 않는다.


void main() {
  var name;

  name = 'Kim';
  name = 1;
  name = 10.0;
}


2. List

void main() {
  List list1 = ['a', 'b', 'c'];
  List<String> list2 = ['a', 'b', 'c'];

  print(list1);        // [a, b, c]
  print(list2);        // [a, b, c]
	print(list1[0]);     // a
  print(list1.length); // 3
}

타입 지정을 안해도 되고 해도 된다.

다른 언어와 같이 index로 접근 가능하며, length 속성도 제공된다.


다음과 같은 속성값을 사용할 수 있다.

  • first
  • last
  • isEmpty
  • isNotEmpty
  • length
  • reversed

단일 요소 추가는 add(element)로, 다른 List의 모든 요소 추가는 addAll(list)로 진행한다.


void main() {
  List users = [
    {'id': 1, 'lastName': "kim"},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  print(users.firstWhere((item) => item['id'] == 2));           // { id: 2, lastName: Park }
  print(users.indexWhere((item) => item['lastName'] == 'Lee')); // 2
}

firstWhere(callback())callback()true를 리턴하는 첫 번째 요소를 리턴한다.

indexWhere(callback())callback()true를 리턴하는 첫 번째 요소의 인덱스를 리턴한다.


void main() {
  List users = [
    {'id': 1, 'lastName': 'Kim'},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  List nums = [10, 20, 30, 40, 50];

  print(users.indexOf({'id': 1, 'lastName': 'Kim'})); // -1
  print(nums.indexOf(20));                            // 1
}

indexOf(element)element가 위치한 인덱스를 리턴한다. 요소 자체가 Map이면 찾지 못한다.


void main() {
  List users = [
    {'id': 1, 'lastName': 'Kim'},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  List nums = [10, 20, 30, 40, 50];

  print(users.contains({'id': 1, 'lastName': 'Kim'})); // -1
  print(nums.contains(20));                            // 1
}

contains(element)element가 List에 포함되어있으면 true, 아니면 false를 리턴한다. 마찬가지로, 요소 자체가 Map이면 찾지 못한다.


void main() {
  List users = [
    {'id': 1, 'lastName': 'Kim'},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  // id: 1, lastName: Kim
  // id: 2, lastName: Park
  // id: 3, lastName: Lee
  users.forEach((item) => print("id: ${item['id']}, lastName: ${item['lastName']}"));

}

JS의 forEach()도 사용 가능하다.


void main() {
  List users = [
    {'id': 1, 'lastName': 'Kim'},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  print(users.map((item) => item['lastName'])); // (Kim, Park, Lee)
}

map()도 JS와 사용 방법이 가능하다.


void main() {
  List<int> nums = [1, 2, 3, 4, 5];

  print(nums.reduce((acc, cur) => acc + cur)); // 15
}

reduce()도 JS의 것과 사용법이 같다. 다만, 아래 fold()와는 달리 acccur의 타입이 같아야 한다.


void main() {
  List users = [
    {'id': 1, 'lastName': 'Kim'},
    {'id': 2, 'lastName': 'Park'},
    {'id': 3, 'lastName': 'Lee'}
  ];

  num res = users.fold<num>(0, (acc, cur) => acc + cur['id']);

  print(res); // 6
}

reduce()는 다음과 같은 전제 조건이 필요하다.

  1. List가 비어있지 않아야 한다.
  2. 모든 요소의 type이 같아야 한다.

하지만, fold()는 reduce()와 작동 방식은 같지만 위와 같은 필요 조건이 없다. 모든 어떻게 생긴 List에서 다 사용 가능하다.


void main() {
  List list = [10, 20, 30, 40, 50];
  list.shuffle();

  print(list);
}

shuffle()은 요소를 랜덤한 순서로 섞는다.


3. Map

void main() {
  Map map1 = {
    'name': 'kim',
    'age': 28
  };

  print(map1); // {name: kim, age: 28}

  Map<String, String> map2 = {
    'name': 'Kim',
    'phone': '010-xxxx-xxxx'
  };

  print(map2); // {name: Kim, phone: 010-xxxx-xxxx}
}

List와 마찬가지로 타입 지정을 해도, 안해도 된다.


void main() {
  Map map = {};

  map.addAll({
    'name': 'kim',
    'age': 28,
    'phone': '010-xxxx-xxxx'
  });

  print(map); // {name: kim, age: 28, phone: 010-xxxx-xxxx}

  map.remove('age');

  print(map); // {name: kim, phone: 010-xxxx-xxxx}
}

추가는 addAll()로, 삭제는 remove()를 사용한다.

addAll()의 파라미터로는 Map이 와야한다. [키-값] 쌍 하나만 넣진 못하고, Map에 담아 전달해야 한다.


void main() {
  Map map = {};

  map.addAll({
    'name': 'kim',
    'age': 28,
    'phone': '010-xxxx-xxxx'
  });

  print(map.keys);            // (name, age, phone)
  print(map.keys.toList());   // [name, age, phone]

  print(map.values);          // (kim, 28, 010-xxxx-xxxx)
  print(map.values.toList()); // [kim, 28, 010-xxxx-xxxx]
}

keys 속성을 통해 key 값들만, values 속성을 통해 value 값들만 가져올 수 있다. toList()를 통해 가져온 key 혹은 value 들을 배열에 담을 수 있다.


다음과 같은 속성 값들을 사용할 수 있다.

  • isEmpty
  • isNotEmpty
  • keys
  • values
  • length


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price['Airpods'] = 390000;
  price['Trackpad'] = 100000;

  print(price); // {iPhone: 1500000, Galaxy: 1000000, Apple Watch: 500000, Airpods: 390000, Trackpad: 100000}
}

가장 간단하게는 위와 같은 방식으로 프로퍼티를 추가할 수 있다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.addAll({
    'airpods': 390000,
    'Trackpad': 100000
  });

  print(price); // { iPhone: 1500000, Galaxy: 1000000, Apple Watch: 500000, airpods: 390000, Trackpad: 100000 }
}

map.addAll(source)을 통해 기존 맵에 source를 추가할 수 있다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.addEntries([
    MapEntry('Airpods', 390000),
    MapEntry('Trackpad', 100000)
  ]);

  print(price); // {iPhone: 1500000, Galaxy: 1000000, Apple Watch: 500000, Airpods: 390000, Trackpad: 100000}
}

혹은 addEntries(mapIterable)을 통해 [키, 값]쌍의 배열을 추가할 수 있다. MapEntry는 [키-값]쌍의 맵을 생성하는 생성자다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.update('iPhone', (prev) => prev * 10);


  print(price); // {iPhone: 15000000, Galaxy: 1000000, Apple Watch: 500000, Macbook: 2500000}
}

update(key, callback(), ifAbsent()) 을 통해 원하는 키의 값을 기존 값에 연산을 적용해 갱신할 수 있다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.update('Airpods', (prev) => prev * 10, ifAbsent: () => 390000);
  price.putIfAbsent('Trackpad', () => 100000);

  print(price); // {iPhone: 1500000, Galaxy: 1000000, Apple Watch: 500000, Airpods: 390000, Trackpad: 100000}
}

update()에 세 번째 파라미터로 콜백 함수를 전달해 해당 키의 프로퍼티가 존재하지 않을 경우, 디폴트 프로퍼티를 추가하는 작업을 수행할 수 있다.

혹은, putIfAbsent(key, callback())을 통해서도 프로퍼티를 추가할 수 있다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.updateAll((key, value) => value.toString() + '원');

  print(price); // {iPhone: 1500000원, Galaxy: 1000000원, Apple Watch: 500000원}
}

updateAll(callback())을 통해 키 혹은 값으로 연산을 적용해 모든 프로퍼티들에 동등한 연산을 적용시켜 새로운 맵으로 만들 수 있다.


void main() {
  Map price = {
    'iPhone': 1500000,
    'Galaxy': 1000000,
    'Apple Watch': 500000,
  };

  price.remove('iPhone');
  price.removeWhere((key, value) => key == 'Galaxy');

  print(price); // {Apple Watch: 500000}
}

프로퍼티는 remove(key) 혹은 removeWhere(callback())으로 제거할 수 있다.

  • remove(key)는 프로퍼티의 키가 key인 것을 제거한다.
  • remove(callback())은 콜백에 조건문을 작성해 해당 조건을 만족하는 프로퍼티를 제거한다.


3. final vs const

void main() {
  final name1 = 'Kim';
  final String name2 = 'Lee';

  const name3 = 'Park';
  const String name4 = 'Choi';
}

final, const 둘 다 상수 표현이다.


void main() {
  final now1 = new DateTime.now();
  print(now1); // 2021-07-25 17:04:02.285

  const now2 = new DateTime.now();
  print(now2); // Error: New expression is not a constant expression.
}

final은 변경만 안된다면 런타임에 값이 지정되도 상관 없다. 하지만, const는 반드시 빌드 타임에 값이 지정되어 있어야 한다.

위 코드에서 now1, now2 둘 다 런타임에 값이 정해지지만, const는 빌드 타임에 값이 지정되어야 하므로 에러를 낸다. 따라서, const는 빌드 타임에 값을 알 수 있을 때에만 사용해야 한다.


4. enum

enum Status {
  approved,
  rejected,
  pending,
}

void main() {
  Status status = Status.approved;

  print(status); // Status.apporved;

  switch(status) {
    case Status.approved:
      print('승인 상태입니다.');
      break;

    case Status.rejected:
      print('거절 상태입니다.');
      break;

    case Status.pending:
      print('대기 상태입니다.');
      break;

    default:
      print('아무 상태도 아닙니다.');
  }
}


5. if, switch

void main() {
  int score = 98;

  if(score >= 90) {
    print('A');
  } else if(score >= 80) {
    print('B');
  } else if(score >= 70) {
    print('C');
  } else {
    print('F');
  }

  String fruit = 'watermelon';

  switch(fruit) {
    case 'apple' :
      print('사과');
      break;
    case 'banana':
      print('바나나');
      break;
    case 'watermelon':
      print('수박');
      break;
    default:
      print('그 외');
  }
}

다른 언어와 사용법이 같다.


6. for문, while문, do~while문

void main() {
  int sum = 0;
  List list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  for (int i = 0; i < list.length; i++) {
    sum += list[i];
  }

  print(sum); // Error: A value of type 'num' can't be assigned to a variable of type 'int'.
}

위 코드는 타입 지정을 안해줘서 에러가 발생한다. List에 숫자를 담으면 기본적으로 num 타입으로 지정되나보다. 다음 중 하나의 방법으로 바꾸면 정상 작동한다.

  1. sum의 타입을 num으로 변경.
  2. list 타입을 int로 변경.


void main() {
  List list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  for (int number in list) {
    print(number); // 1 2 3 4 5 6 7 8 9 10
  }
}

for of 구문도 지원되지만, 인덱스가 아니라 값을 순회한다는 점을 주의하자.

while문과 do~while문은 다른 언어의 사용법과 같다.


7. 함수

double add(double a, double b) { return a + b; }

함수는 위 방법처럼 파라미터의 타입과 리턴 타입을 명시적으로 적어줘야 한다.


Optional Parameter

void main() {
  print(add(1, 2, 3)); // 6
  print(add(1, 2));    // 13
}

double add(double a, double b, [double option = 10.0]) {
  return a + b + option;
}

혹은 위와 같이 Optional Parameter를 사용할 수도 있다.


Named Parameter

void main() {
  print(add(1, 2, c: 3)); // 6
  print(add(1, 2));    // 13
}

double add(double a, double b, {double c}) { // Error: The parameter 'c' can't have a value of 'null' because of its type 'double', but the implicit default value is 'null'.
  return a + b + c;
}

위와 같이 중괄호 { } 안에 Named parameter를 지정할 수 있는데, 강의에서와 같이 변수 명만 사용하면 위와 같은 에러가 출력된다. c에 디폴트 값을 주니까 정상 작동하게 되는데, 이럼 Optional parameter와 무슨 차이인지 잘 모르겠다.


8. typedef

typedef Operation(int x, int y);

void main() {
  calc(1, 2, add); // 덧셈 결과: 3
  calc(1, 2, sub); // 뺄셈 결과: -1
}

void add(int x, int y) { print('덧셈 결과: ${x + y}'); }
void sub(int x, int y) { print('뺄셈 결과: ${x - y}'); }
void calc(int x, int y, Operation oper) { oper(x, y); }

typedef는 콜백 인터페이스를 정의한다. C언어에서와 같이 alias를 붙이는 용도(가령, typedef long long ll; 와 같이)로는 사용할 수 없다.


9. Class

기본 생성자

class User {
  final userName;
  final userAge;

  User(String name, int age)
      : this.userName = name,
        this.userAge = age;

  void printUserInfo() {
    print('이름은 ${this.userName}이고, 나이는 ${this.userAge}살입니다.');
  }
}

void main() {
  User user = new User('Kim', 28);

  user.printUserInfo(); // 이름은 Kim이고, 나이는 28살입니다.
}

생성자는 조금 다른 규칙을 띈다. 중괄호 안에 멤버 변수 할당을 하는 것이 아니라, 콜론(:)을 사용해 독특한 방법으로 할당한다.


이름 붙여진 생성자

class User {
  final userName;
  final userAge;

  User(String name, int age)
      : this.userName = name,
        this.userAge = age;

  User.fromMap(Map map)
      : this.userName = map['name'],
        this.userAge = map['age'];

  void printUserInfo() {
    print('이름은 ${this.userName}이고, 나이는 ${this.userAge}살입니다.');
  }
}

void main() {
  User user1 = new User('Kim', 28);
  User user2 = new User.fromMap({'name': 'Lee', 'age': 18});

  user1.printUserInfo(); // 이름은 Kim이고, 나이는 28살입니다.
  user2.printUserInfo(); // 이름은 Lee이고, 나이는 18살입니다.
}

위 코드는 다른 생성자를 선언해 사용한 코드다. 클래스명.함수명()의 방식으로 새로운 생성자를 지정할 수 있다. 또한, 클래스명만 적힌 기본 생성자 없이 다른 생성자를 사용할 수 있다.


private 변수

class User {
  final userName;
  final userAge;
  final _userId;

  User(String name, int age, int id)
      : this.userName = name,
        this.userAge = age,
        this._userId = id;
}

void main() {
  User user1 = new User('Kim', 28, 19384029);

  print(user1._userId); // 19384029
}

private 접근 지정자는 변수명 앞에 _(underscore)를 붙여주면 된다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5f289b66-23f7-4d17-977a-d6ebc14a3e48/스크린샷_2021-07-25_오후_6.12.01.png

Java에선 같은 클래스 내부가 아니면 접근을 못하지만, Dart는 같은 파일 내부라면 접근할 수 있다고 한다. 다시 말하면, Dart에선 private 변수 및 메서드는 파일 단위로 감춰진다.


getter, setter

class User {
  final userName;
  final userAge;
  int _userId;

  User(String name, int age, int id)
      : this.userName = name,
        this.userAge = age,
        this._userId = id;

  int get userId {
    return this._userId;
  }

  set userId(int id) {
    this._userId = id;
  }
}

void main() {
  User user = new User('Kim', 28, 19384029);
  user._userId = 49602939;

  print(user._userId); // 49602939
}

getter는 앞에 get 예약어를, setter는 앞에 set 예약어를 붙인다. getter는 앞에 리턴 타입도 명시해줘야 한다.


상속

class Car {
  final type;
  final model;
  final color;

  Car(String type, String model, String color)
      : this.type = type,
        this.model = model,
        this.color = color;
}

class Bus extends Car {
  final capacity;

  Bus(String type, String model, String color, int capacity)
      : this.capacity = capacity,
        super(type, model, color);

  void printInfo() {
    print(
        "이 버스의 타입은 ${this.type}, 모델명은 ${this.model}, 컬러는 ${this.color}, 수용 인원은 ${this.capacity}입니다.");
  }
}

void main() {
  Bus county = new Bus("Hyundai", "County", "white", 25);

  county.printInfo(); // 이 차의 타입은 Hyundai, 모델명은 County, 컬러는 white, 수용 인원은 25입니다.
}


오버라이딩

class Car {
  final type;
  final model;
  final color;

  Car(String type, String model, String color)
      : this.type = type,
        this.model = model,
        this.color = color;

  void printInfo() {
   print("이 차의 타입은 ${this.type}, 모델은 ${this.model}, 컬러는 ${this.color}입니다.");
  }
}

class Bus extends Car {
  final capacity;

  Bus(String type, String model, String color, int capacity)
      : this.capacity = capacity,
        super(type, model, color);

  @override
  void printInfo() {
    print("이 버스의 타입은 ${this.type}, 모델명은 ${this.model}, 컬러는 ${this.color}, 수용 인원은 ${this.capacity}입니다.");
  }
}

void main() {
  Car focus = new Car("Ford", "Focus", "red");
  Bus county = new Bus("Hyundai", "County", "white", 25);

  focus.printInfo();  // 이 차의 타입은 Ford, 모델은 Focus, 컬러는 red입니다.
  county.printInfo(); // 이 버스의 타입은 Hyundai, 모델명은 County, 컬러는 white, 수용 인원은 25입니다.
}


class Car {
  final type;
  final model;
  final color;

  Car(String type, String model, String color)
      : this.type = type,
        this.model = model,
        this.color = color;

  void printInfo() {
    print("이 차의 타입은 ${this.type}, 모델은 ${this.model}, 컬러는 ${this.color}입니다.");
  }
}

class Bus extends Car {
  final capacity;

  Bus(String type, String model, String color, int capacity)
      : this.capacity = capacity,
        super(type, model, color);

  @override
  void printInfo() {
    super.printInfo();
    print("이 차는 버스입니다.");
  }
}

void main() {
  Bus county = new Bus("Hyundai", "County", "white", 25);

  county.printInfo(); // 이 차의 타입은 Hyundai, 모델은 County, 컬러는 white입니다.\n이 차는 버스입니다.
}

혹은 위와 같이 super 예약어를 통해 부모 메서드를 부를 수 있다.


static 멤버 변수

class Car {
  static String brand = "";
  final model;

  Car(String model) : this.model = model;

  void printInfo() {
    print("${brand}${this.model}입니다.");
  }
}

void main() {
  Car.brand = "Hyundai";
  Car car1 = new Car("Avante");
  Car car2 = new Car("Sonata");

  car1.printInfo(); // Hyundai의 Avante입니다.
  car2.printInfo(); // Hyundai의 Sonata입니다.

  Car.brand = "Kia";
  Car car3 = new Car("k3");
  Car car4 = new Car("k5");

  car3.printInfo(); // Kia의 k3입니다.
  car4.printInfo(); // Kia의 k5입니다.
}

static 멤버 변수는 초기화가 필수다. this 키워드를 붙여 부르면 안된다. 모든 객체가 공유하므로


10. 인터페이스

class Car {
  void printType() {}
}

class Bus implements Car {
  void printType() { print("버스입니다."); }
}

class Taxi implements Car {
  void printType() { print("택시입니다."); }
}

void main() {
  Bus bus = new Bus();
  Taxi taxi = new Taxi();

  bus.printType();  // 버스입니다.
  taxi.printType(); // 택시입니다.
}

Interface 키워드는 없고, Class 키워드를 그대로 사용한다. implements 키워드를 사용해 구현할 대상 클래스를 명시하고, 작성되지 않은 함수를 작성해줘야만 한다.


11. Cascade Operator

class Car {
  final type;
  String model;

  Car(String type, String model)
      : this.type = type,
        this.model = model;

  printType() {
    print("타입은 ${this.type}입니다.");
  }

  printModel() {
    print("모델은 ${this.model}입니다.");
  }
}

void main() {
  Car car = new Car("Hyundai", "Avante");

  car.printType();  // 타입은 Hyundai입니다.
  car.printModel(); // 모델은 Avante입니다.

  new Car("Hyundai", "Sonata")
    ..printType()
    ..printModel(); // 타입은 Hyundai입니다.\n모델은 Sonata입니다.

  Car car3 = new Car("Hyundai", "Grandeur")..printType()..printModel(); // 타입은 Hyundai입니다.\n모델은 Grandeur입니다.

  car3.model = "Genesis";

  car3.printModel(); // 모델은 Genesis입니다.
}

.. 을 통해 여러 메서드를 한 번에 호출할 수 있다.

  • 두 번째 Car 객체는 익명 객체로 사용되었다.
  • 세 번째 Car 객체는 생성과 동시에 여러 메서드를 호출했으며, 할당된 객체를 car3이라는 참조 변수가 가리키도록 했다. 이후 car3의 멤버 변수 model이 잘 변경되는 모습을 확인할 수 있다.

카테고리:

업데이트:

댓글남기기