JS 와 비슷하게
var 변수명 = 값;
형식으로 선언할 수 있다.
Dart 에서는 변수에 값이 들어가면 자동으로 타입을 추론하는 타입 추론 기능을 제공하므로 명시적으로 타입을 선언하지 않아도 된다.
앞서 말한 var은 변수의 타입을 유추한 뒤 해당 변수에 추론한 타입을 고정시켜 적용한다.
따라서 고정된 변수 타입과 다른 변수 타입의 값을 같은 이름의 변수에 다시 저장하려 하면 에러가 일어난다.
그러나 dynamic 키워드를 사용하면 변수의 타입이 고정되지 않아 다른 타입의 값을 저장할 수 있다.
void main() {
dynamic name = '코드팩토리';
name = 1; // 이제 name = 1 이다.
print(name); // 1이 잘 출력된다
}
둘다 변수 선언 후 값을 변경할 수 없다. 그럼 차이가 뭘까?
final 은 runtime, const는 compile time 상수이다.
즉, final은 코드가 실행될 때 값이 확정이 되고
const는 컴파일 시에 값이 확정이 된다.
일반적으로 고정된 값을 사용하고 싶으면 const 를 사용하면 되고
프로그램 실행시에 결정되는 값이 필요하면 final을 사용하면 된다. (나중에 비동기 함수를 사용할 때, 쓰일 것 같다)
- 추가로 final은 컴파일 타임, 런타임 모두 수용 가능하나, 성능 최적화를 하려면 컴파일 타임에 정해지는 상수는 const를 사용하는 것이 좋다.
final
void main() {
final DateTime now = DateTime.now();
print(now);
}
DateTime.now() 함수는 현재 시간을 가져오는 함수이다.
DateTime.now() 함수를 호출할 때(런타임 에) now 변수의 값이 현재 시간으로 초기화 된다.
const
void main() {
// 에러
const DateTime now = DateTime.now();
print(now);
}
const로 지정한 변수는 컴파일 타임에 값을 정해주어야 하는데 now의 값을 정하려면 일단 코드를 빌드(run) 해서 DateTime.now() 함수를 실행시켜야 한다.
따라서 컴파일 타임에 값을 정해줄 수 가 없고, 오류가 발생한다.
앞서 var로 자동 추론 타입 변수를 생성 할 수 있었다.
근데 이러면 유지 보수가 힘드니 직접 변수 타입을 지정해 주는게 좋다.
int, string, 등등...
여러 값을 하나의 변수에 저장할 수 있는 타입
List, Map, Set 등이 있다.
컬렉션 타입은 서로의 타입으로 자유롭게 형 변환이 가능하다
우리가 평소에 많이 봐왔던 그 list가 맞다
add(), where(), map(), reduce() 메소드를 제공한다
리스트 맨 끝에 값을 추가할 때 사용
리스트에 있는 값들을 순서대로 순회하면서 특정 조건에 맞는 값만 필터링 하는 데 사용.
매개변수에 함수를 입력해야 하며, 입력된 함수는 기존 값을 하나씩 매개변수로 입력받는다.
함수가 각 값별로 true를 반환하면 리스트에서 그 값을 유지하고, false를 반환하면 그 값을 리스트에서 버린다.
순회가 끝나면 true인 값들로 된 iterable을 반환한다.
iterable 이란?
추상 클래스로 List 나 Set 등의 컬렉션 타입들이 상속받는 클래스이다.
즉, 컬렉션 타입들이 공통으로 사용하는 기능을 정의해둔 클래스이다.
where(), map() 등 순서가 있는 값을 반환할 때 사용한다.
void main() {
List<String> pokeList = ['잠만보', '피카츄', '메타몽', '뮤'];
final newList = pokeList.where(
(name) => name == '잠만보' || name == '메타몽', // pokeList에서 '잠만보' '메타몽' 뺴고 다 삭제함
);
print(newList);
print(newList.toList()); // Iterable을 List로 다시 변환할 때 .toList() 사용
}
실행결과
(잠만보, 메타몽)
[잠만보, 메타몽]
map() 함수는 리스트에 있는 값들을 순서대로 순회하면서 값을 변경할 수 있다.
매개변수에 함수를 입력해야 하며 입력된 함수는 기존 값을 하나씩 매개변수로 입력받는다.
함수가 반환하는 값이 현재의 값을 대체하며 순회가 끝나면 iterable이 반환된다.
void main() {
List<String> pokeList = ['잠만보', '피카츄', '메타몽', '뮤'];
final newPokeList = pokeList.map(
(name) => '포켓몬 $name', // 리스트의 모든 값 앞에 ‘포켓몬’을 추가
);
print(newPokeList);
// Iterable을 List로 다시 변환하고 싶을 때 .toList() 사용
print(newPokeList.toList());
}
실행결과
(포켓몬 잠만보, 포켓몬 피카츄, 포켓몬 메타몽, 포켓몬 뮤)
[포켓몬 잠만보, 포켓몬 피카츄, 포켓몬 메타몽, 포켓몬 뮤]
리스트에 있는 값들을 순회하면서 매개변수에 입력된 함수를 실행한다.
reduce 함수는 순회할 때 마다 값을 쌓아가는 특징이 있다.
특이하게 reduce 함수는 iterable이 아닌 리스트 멤버의 타입과 같은 타입을 반환한다.
void main() {
List<String> pokeList = ['잠만보', '피카츄', '메타몽', '뮤'];
final allMembers = pokeList.reduce((value, element) => value + ', ' + element); // ➊ 리스트를 순회하며 값들을 더합니다.
print(allMembers);
}
실행 결과
잠만보, 피카츄, 메타몽, 뮤 // 리스트 안에 값이 String 이므로 리스트가 아니라 String 을 반환하는 것을 알 수 있다.
매개변수로 준 익명함수 만 보자
(value, element) => value + ', ' + element
value 에는 pokeList의 첫번째 값 '잠만보' 가 들어가고
element 에는 pokeList의 두번째 값 '피카츄' 가 들어간다.
이후value + ', ' + element에 의해잠만보 , 피카츄가 반환되고 이 반환값이 다시 value 로 들어간다. 그리고 pokeList의 세번째 값 '메타몽' 이 element로 들어간다.
pokeList의 모든 값을 순회할 때 까지 계속된다.
reduce와 동일한 논리로 실행된다. 단지 다른 점이 있다면 reduce 는 함수가 실행되는 리스트 요소들의 타입이 같아야 하지만 fold 는 어떠한 타입도 반환할 수 있다.
void main() {
List<String> pokeList = ['잠만보', '피카츄', '메타몽', '뮤'];
// ➊ reduce() 함수와 마찬가지로 각 요소를 순회하며 실행됩니다.
final allMembers =
pokeList.fold<int>(0, (value, element) => value + element.length);
print(allMembers); // pokeList 안에 있는 포켓몬들의 이름 글자수의 총합 출력
// 3 + 3 + 3 + 1 = 10
}
key : value 쌍을 저장함 (JS 의 객체, python의 dictionary)
Map<key 타입, value타입> Map 이름 형식으로 선언함
void main() {
Map<String, String> dictionary = {
'Harry Potter': '해리 포터', // 키 : 값
'Ron Weasley': '론 위즐리',
'Hermione Granger': '헤르미온느 그레인저',
};
print(dictionary['Harry Potter']);
print(dictionary['Hermione Granger']);
}
// 실행결과
해리 포터
헤르미온느 그레인저
map이 key : value 의 조합이라면
Set은 중복없는 값들의 집합이다.
Set<타입> 세트이름 형식으로 선언함
void main() {
Set<String> pokeList = {'잠만보', '피카츄', '메타몽', '뮤','잠만보'}; // ➊ 잠만보 중복
print(pokeList);
print(pokeList.contains('피카츄')); // ➋ 값이 있는지 확인하기
print(pokeList.toList()); // ➌ 리스트로 변환하기
List<String> pokeList2 = ['잠만보', '뮤', '뮤'];
print(Set.from(pokeList2)); // ➍ List 타입을 Set 타입으로 변환
}
// 실행결과
{잠만보, 피카츄, 메타몽, 뮤}
true
[잠만보, 피카츄, 메타몽, 뮤]
{잠만보, 뮤}
Set은 절대 중복을 허용하지 않는다. ➊ 에서 보면 '잠만보'가 중복되었지만 하나만 출력된다.
enum 은 한 변수의 값을 몇가지 옵션으로 제한하는 기능이다. 선택지가 제한적일 때 사용한다.
코드 상에서 String을 직접 비교하는 방법은 좋은 코딩 방식이 아니다.
왜 ??
1. 오타가 발생할 수 있다 ("apple" 을 "aple"로 입력)
2. 유지 보수가 힘들다. 일일히 다 수정해야 함
3. 가독성이 안좋다. ("aple"을 어캐 찾냐)
등등의 이유로...
되도록 enum을 사용하자.
enum Status {
approved,
pending,
rejected,
}
void main() {
Status status = Status.approved;
print(status); // Status.approved
}
enum은 자동완성을 지원하고 정확히 어떤 선택지가 존재하는지 정의 할 수 있기 때문에 편하다.
익명함수와 람다함수는 둘다 이름이 없고 일회성으로 사용된다.
dart 에서는 익명함수와 람다함수의 구분이 없다. (보통의 언어는 구분함)
// 형식
(파라미터) {
함수 바디
}
void main() {
List<int> numbers = [1,2,3,4,5];
// 일반 함수로 모든 값 더하기
final allMembers = numbers.reduce((value, element) {
return value + element;
});
print(allMembers);
}
// 실행결과
15
JS의 화살표 함수
익명함수에서 {} 빼고 => 기호를 추가하면 람다함수임
// 기본 형식
(파라미터) => 하나의 statement
void main() {
List<int> numbers = [1,2,3,4,5];
// 람다 함수로 모든 값 더하기
final allMembers = numbers.reduce((value, element) => value + element);
print(allMembers);
}
// 실행결과
15
dart 3.0 에서 등장한 새로운 타입이다.
포지셔널 파라미터 / 네임드 파라미터 중 한가지 방식을 적용하여 사용할 수 있다.
포지셔널 파라미터를 이용한 레코드는 포지셔널 파라미터로 표시한 타입 순서를 반드시 지켜야 한다.
형식
(타입1, 타입2, ...) 레코드이름 = ('값1', '값2', ...);
다음은 String, int 순서로 데이터를 입력해야 하는 레코드를 선언한 예이다.
void main() {
(String, int) pokemon = ('잠만보', 100);
print(pokemon);
}
// 실행결과
(잠만보, 100)
만일 레코드에 정해진 순서를 따르지 않으면 에러가 발생한다.
void main() {
// Invalid Assignment 에러
(String, int) pokemon = (100, '잠만보'); // (String int) 순서가 아니므로 에러
print(pokemon);
}
레코드에 정의할 수 있는 값의 개수에는 제한이 없다. 2개이상 해도 된다.
레코드의 특정 값을 가져오고 싶으면 $ 기호를 사용하면 된다
배열의 인덱스 생각하면 된다. 단, 인덱스가 1부터 시작한다.
void main() {
(String, int, bool) pokemon = ('잠만보',100,false);
print(pokemon.$1);
print(pokemon.$2);
print(pokemon.$3);
}
// 실행결과
잠만보
100
false
네임드 파라미터는 포지셔널 파라미터와는 다르게 입력 순서를 지킬 필요가 없다.
형식
({변수타입1 변수이름1, 변수타입2 변수이름2, ...}) 레코드 이름 = (name: '잠만보', level: 99 ...);
void main() {
// Named Parameter 형태로 Record를 선언하는 방법이다.
// 다른 Named Parameter와 마찬가지로 순서는 상관이 없어진다.
({String name, int level}) pokemon = (name: '잠만보', level: 99);
// (level: 99, name: 잠만보) 출력
print(pokemon);
}
// 실행결과
(level: 99, name: 잠만보)
구조분해는 값을 반환받을 때, 반환된 타입을 그대로 복제해서 타입 내부에 각각의 값을 직접 추출해오는 문법이다.
JS의 구조분해 문법과 동일
형태
final [변수이름1, 변수이름2, ...] = [새로운 이름1, 새로운 이름2];
// 그냥 쉽게말해서
변수이름1 = 새로운 이름1;
변수이름2 = 새로운 이름2;
.
.
.
이렇게 계속하기 귀찮으니까 한번에 리스트로 할당하는 것이다.
예시
void main() {
// 아래 코드와 같지만 한줄에 해결 할 수 있다.
// final newJeans = ['민지', '해린'];
// final minji = newJeans[0];
// final haerin = newJeans[1];
final [minji, haerin] = ['민지', '해린'];
// 민지 출력
print(minji);
// 해린 출력
print(haerin);
}
Spread 문법은 ... 을 붙이면 된다.
배열의 요소를 펼치는 문법이다.
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8];
// spread operator를 사용하게 되면 중간의 값들을 버릴 수 있다.
final [x, y, ..., z] = numbers;
// 1 출력
print(x);
// 2 출력
print(y);
// 8 출력
print(z);
}
배열에서의 구조분해와 비슷하다.
형태
// key에 맞는 value가 각각 다시 할당된다고 생각하면 된다.
final {'name': name, 'level': level} = {'name': '잠만보','level':90};
예시
void main() {
final pokemon = {'name': '잠만보','level':90};
// Map의 구조와 똑같은 구조로 Destructuring하면 된다.
final {'name': name, 'level': level} = pokemon;
// name: 잠만보
print('name: $name');
// level: 90
print('level: $level');
}
// 실행결과
name: 잠만보
level: 90
void main() {
final Pokemon pikachu = Pokemon('피카츄', 20);
// 클래스의 생성자 구조와 똑같이 구조 분해하면 된다.
final Pokemon (name:name, level:level) = pikachu;
print(name);
print(level);
}
class Pokemon {
final String name;
final int level;
Pokemon(this.name, this.level);
}
// 실행결과
피카츄
20
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
void main() {
var person = Person('Alice', 30);
// 속성을 직접 참조
String name = person.name;
int age = person.age;
// 결과를 출력
print('Name: $name, Age: $age');
}
// 실행결과
Name: Alice, Age: 30
위의 예제에서는 person 객체에서 name과 age 속성을 각각 직접 참조해야 한다.
코드가 단순해 보이지만, 속성이 많아질 경우 매번 같은 방식으로 추출해야 하는 번거로움이 있다.
dart 3.0 으로 업데이트 되면서
코드는 표현식(expression)과 문(statement) 으로 나눌 수 있다.
표현식이란 어떠한 값을 만들어 내는 코드이다.
1 + 1 = 2 에서 1 + 1 은 2라는 값을 만들어내는 표현식이다.
표현식이 평가되면 새로운 값을 생성하거나 기존 값을 참조한다.
문 은 기본 단위이자 가장 작은 코드 실행단위로 명령문 즉, 컴퓨터에 내리는 명령이라고 생각하면 된다.
var a = 3 같은 것이 문이다.
표현식 여러개가 모여서 문이 되며, 문에는 선언문, 할당문, 반복문 등이 있다.
void main() {
String dayKor = '월요일';
String dayEnglish = switch (dayKor) {
'월요일' => 'Monday',
'화요일' => 'Tuesday',
'수요일' => 'Wednesday',
'목요일' => 'Thursday',
'금요일' => 'Friday',
'토요일' => 'Saturday',
'일요일' => 'Sunday',
_ => 'Not Found',
};
print(dayEnglish);
}
// 실행결과
Monday
switch case 문의 조건식에 숫자 이외의 조건이 들어간다?!
void switcher(dynamic anything) {
switch (anything) {
case 'aaa':
print('match: aaa');
break;
case [1, 2]:
print('match: [1, 2]');
break;
case [_, _, _]:
print('match [_,_,_]');
break;
case [int a, int b]:
print('match: [int $a, int $b]');
break;
case (String a, int b):
print('match: (String: $a, int: $b)');
break;
default:
print('no match');
}
}
void main() {
switcher('aaa');
switcher([1, 2]);
switcher([3, 4, 5]);
switcher([6, 7]);
switcher(('민지', 19));
switcher(8);
}
// 실행결과
match: aaa
match: [1, 2]
match [_,_,_]
match: [int 6, int 7]
match: (String: 민지, int: 19)
no match
엄격한 검산느 코드가 입력받을 수 있는 모든 조건을 전부 확인하고 있는지 체크하는 기술이다.
dart 3.0에서는 switch문에 엄격한 검사가 추가되어 모든 조건을 확인하고 있는지 빌드할 때, 확인할 수 있다.
void main(){
// bool타입 val에 입력될 수 있는 값은 true, false, null 이다.
bool? val;
// null 조건을 입력하지 않았기 때문에 non exhaustive switch statement 에러가 발생한다.
// null case를 추가하거나 default case 를 추가해야 에러가 사라진다.
switch(val){
case true:
print('true');
case false:
print('false');
};
}
switch 문에 when 키워드로 보호 구문을 추가할 수 있다.
when 키워드는 boolean 으로 반환할 조건을 각 case 문에 추가할 수 있으며, when 키워드 뒤에 오는 조건이 true를 반환하지 않으면 case 매치가 안된다.
void main() {
(int a, int b) val = (1, -1);
switch (val) {
case (1, _) when val.$2 > 0:
print('1, 2');
break;
default:
print('default');
}
}
// 실행결과
default
만약 when 뒤에 조건을 만족하면 1,2 가 출력된다.
void main() {
(int a, int b) val = (1, 3);
switch (val) {
case (1, _) when val.$2 > 0:
print('1, 2');
break;
default:
print('default');
}
}
// 실행결과
1, 2
base 제한자는 base 클래스의 기능을 강제하는 제한자이다.
base 키워드를 사용하게 되면 해당 클래스는 오직 상속만 할 수 있게 된다.
그리고 base 클래스가 아닌 자식 클래스는 꼭 base, final 또는 sealed 제한자를 함께 사용해 주어야 한다.
base class Parent{}
import '1_a.dart';
// 인스턴스화 가능
Parent parent = Parent();
// 가능
base class Child extends Parent{}
// subtype of base or final is not base final or sealed 에러
// base / sealed / final modifier중 하나 필요
class Child2 extends Parent{}
// base class Child2 extends Parent{}
// subtype of base or final is not base final or sealed 에러
// base 클래스는 implement 불가능
class Child3 implements Parent{}
final 제한자를 사용하면 같은 파일에서 상속과 재정의를 할 수 있지만 외부 파일에서는 할 수 없다.
final 제한자는 base 제한자 의 기능을 모두 포함한다.
final class Parent{}
import '2_a.dart';
// 인스턴스화 가능
Parent parent = Parent();
// extend 불가능
class Child extends Parent{}
// implement 불가능
class Child2 implements Parent{}
interface 제한자는 클래스를 외부 파일에서 상속받지 못하고 재정의만 할 수 있도록 제한하는 역할을 한다.
interface class Parent{}
import '3_a.dart';
// 인스턴스화 가능
Parent parent = Parent();
// extend 불가능
class Child1 extends Parent{}
// implement 가능
class Child2 implements Parent{}
sealed 제한자는 sealed 클래스를 파일 외부에서 상속, 재정의, 그리고 인스턴스화 할 수 없도록 제한한다.
sealed class Parent{}
import '4_a.dart';
// 인스턴스화 불가능
Parent parent = Parent();
// extend 불가능
class Child1 extends Parent {}
// implement 불가능
class Child2 implements Parent {}
일반 mixin문법과 같이 동작하고 상속도 할 수 있다.
mixin class MixinExample{}
// extend 가능
class Child1 extends MixinExample{}
// mixin으로 사용 가능
class Child2 with MixinExample{}