[Dart] Dart 문법
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()
와는 달리 acc와 cur의 타입이 같아야 한다.
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()
는 다음과 같은 전제 조건이 필요하다.
- List가 비어있지 않아야 한다.
- 모든 요소의 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 타입으로 지정되나보다. 다음 중 하나의 방법으로 바꾸면 정상 작동한다.
sum
의 타입을 num으로 변경.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)를 붙여주면 된다.
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
이 잘 변경되는 모습을 확인할 수 있다.
댓글남기기