CH.2 다트
Chapter Point
- 다트의 Hello, World
- 다트 프로그램 해부
- 제어 흐름, 루프, 함수 등 기본 문법
- 객체지향 프로그래밍
- I/O 라이브러리 사용법
1. Hello, Dart
void main() {
print("Hello, Dart!");
}
1.1 다트 프로그램 해부
![](https://blog.kakaocdn.net/dn/IWAmV/btsELrKb5Yt/NGRXYu9n94EDmahUA8DbEK/img.png)
- main은 다트 프로그램에서 항상 처음으로 실행되는 코드이다.
- 모든 프로그램에는 main함수를 포함해야 한다.
- 반환 타입은 void로 반환타입이 없는 경우를 뜻한다.
- 코드는 세미콜론(;)으로 끝난다. 다트의 모든 행은 세미콜론으로 마치는 것이 규칙
1.2 함수 호출및 간단한 리팩토링
Hello, World!
Hello, Catsbi!
Hello, Earth!
Hello, Pipo!
Hello, Crong!
1. 무작정 출력하기
void main() {
print("Hello, World!");
print("Hello, Catsbi!");
print("Hello, Earth!");
print("Hello, Pipo!");
print("Hello, Crong!");
}
5개의 문자를 무작정 print로 출력해 보았다. 하지만, 5개는 이렇게 했다지만 출력해야할 문자가 100개 1000개쯤 되면, 너무 비효율적이다. 그렇기에 우선 출력을 담당할 함수를 만든다.
2. 출력 함수 만들기
void main() {
helloDart(); //함수 호출
}
void helloDart() {
print('Hello, Dart');
}
void main() {
helloDart("World!");
helloDart("Catsbi!");
helloDart("Earth!");
helloDart("Pipo!");
helloDart("Crong!");
}
void helloDart(String name) {
print('Hello, $name');
}
void main() {
List<String> greetings = ["World!", "Catsbi!", "Earth!", "Pipo!", "Crong!"];
for (var name in greetings) {
helloDart(name);
}
}
void helloDart(String name) {
print('Hello, $name');
}
이렇게 각각의 이름을 배열로 모은 뒤 반복문을 통해 각각의 이름을 함수에 전달해 출력하도록 리팩토링을 완료해봤다.
1.3 입출력과 다트 라이브러리
개발환경 구축시 설치한 Dart-SDK 는 다양한 라이브러리를 제공하는데, 이 중 dart:core 라이브러리만 자동으로 로드된다. 그래서 그밖에 dart:html, dart:async, dart:mach 등 다양한 라이브러리를 사용하기 위해서는 직접 로드를 해줘야 한다. 그 방법은 아래와 같다.
import 'dart:io';
입출력을 지원하는 I/O 라이브러리를 로드하는 코드다.
1.2 다트의 프로그래밍 개념
- 다트는 객체지향 언어이며 단일 상속(single inheritance)을 지원한다.
- 다트에서 모든 것이 객체이며 모든 객체는 클래스의 인스턴스다. 모든 객체는 Object 클래스를 상속받는다. (숫자도 기본형이 아닌 객체다.)
- 다트는 형식을 갖는다(typed) 문자열을 반환하다고 선언한 함수에서 숫자를 반환할 수 없다.
- 다트는 최상위 수준 함수와 변수를 지원하며 이를 라이브러리 멤버라 부른다.
- 다트는 어휘적(exically)으로 한정된다.
1.2.1 다트의 형식 시스템
변수를 선언할 때 형식을 지정해야 한다.
String name;
int age;
형식을 사용하면 해당 변수에 들어갈 값의 형식을 지정해줄 수 있는데, 이처럼 변수의 형식을 지정해주면 그외의 형식으로는 값을 대입할 수 없다.
int age = 10; // 문제없이 대입
int price = "천원"; // int 타입의 값 변수의 String을 대입하니 에러
List<String> names; //문자열 리스트
List<int> ages; //정수 리스트
Map<String, int> people;//문자열 키와 정수 값을 갖는 맵
int addNumber10(int number) {
return number + 10;
}
매개변수에 10을 더해 반환하는 함수(addNumber10)다. 반환형식은 void가 아닌 int이기에 int형식의 값이 반환된다는 것을 알 수 있다.
dynamic myNumber = 'Hello';
var myString = 'Hello';
myPrint() {
print('hello');
}
이렇게 형식을 생략한 함수는 컴파일러가 함수의 형식을 자동으로 추론하는데, 위 myPrint함수는 값을 반환하지 않기 때문에 반환값을 변수에 할당하면 오류가 발생한다.
언제 동적 형식을 사용할까?
Map<String, dynamic> human;
동적 형식 키워드중에는 var도 있다는 것을 알고 있지만, 이 키워드는 변수를 정의할때만 사용할 수 있다. var를 사용할 수 있는 범위는 한정적이며 되도록 정적 형식을 사용하는 것이 좋다.
1.2.2 주석
// 인라인 주석
/*
블록 주석. 다트의 블록 주석은 그리 편하지 않다.
*/
///문서
///
/// 클래스의 문서화에 사용하는 주석
1.2.3 변수와 할당
String name;
name이라는 변수를 선언했으며 값은 할당되지 않은 상태이기에 null 값을 갖는다 코드 스타일 가이드에 따라 명시적으로 null을 할당하지는 말자.
final, const, static
const String name = 'Catsbi';
const String name2 = 'Catsbi $secondName'; //Error
const 키워드는 변수의 값을 바꿀수 없도록(immutable)하는 키워드로 컴파일 이후에는 변경될 수 없어야 한다. 그렇기에 위 코드에서 두 번째 코드는 컴파일 이후에 템플릿 리터럴($secondName)에 의해 변경될 수 있기 때문에 사용이 불가하다.
class Person {
final String name;
Person(this.name);
}
void main() {
Person p = new Person("Catsbi");
print(p.name);
}
class Car {
final String name;
static const wheelCount = 4;
Car(this.name);
static void printExhaustsound(){
print("부릉 부릉");
}
}
void main() {
print(Car.wheelCount);// 4
Car.printExhaustsound();// 부릉 부릉
}
1.2.4 널 인지 연산자(null-aware)
?., ??, ??=
이 객체가 null이면 오류를 발생하지도 말고, 아무것도 하지말라고 지시하는 연산자.
?. 연산자
API를 통해 유저 정보를 받아온다고 할 때 사용자 정보의 유무를 확인 할 수 없는 경우 다음과 같이 코드를 작성한다.
void getUserAge(String username) async {
final request = new UserRequest(username);
final response = await request.get();
User user = new User.fromResponse(response);
if (user != null) {
this.userAge = user.age;
}
// 생략
}
조건문 if를 통해 user 가 null인지 확인을 한 뒤 유저 정보중 나이(age)를 할당 해주는데, 널-인지 연산자(?.)를 사용하면 더 간결하게 코드 작성이 가능하다.
void getUserAge(String username) async {
...
this.userAge = user?.age;
...
}
이와 같이 작성을 하면 user가 null이면 오류를 발생하지 않고 userAge에 null을 할당하고 아닐 경우 정상적으로 유저의 나이를 대입한다.
?? 연산자
해당 값이 없는 경우 에러를 발생시키지 않는 것까지는 좋다. 하지만 여기서 더 나아가 null이 아닌 default value를 넣어주고 싶다면 ?? 연산자를 쓰면 된다.
void getUserAge(String username) async {
...
this.userAge = user.age ?? 18;
...
}
??= 연산자
객체가 null이면 기본값을 할당하고 아니면 객체를 그대로 반환하는 연산자.
int x = 5
x ??= 3;
1.3 제어 흐름
Dart는 다른 언어들과 마찬가지로 조건문, 반복문 등 여러 문법을 통해 로직의 흐름을 제어할 수 있다.
1.3.1 if와 else
대부분 언어와 같이 다트 역시 조건문 (if, else if, else)를 지원한다.
Cup cup = new Cup();
cup.set(getDrink());
if(cup.getDrinkType() == "Coffee"){
print("커피입니다");
} else if(cup.getDrinkType() == "Orange Juice") {
print("오렌지 주스입니다.");
} else {
print("알 수 없는 음료입니다.");
}
- if(평가식){ 코드 }
: if문은 평가식을 평가 한 뒤 참인지 거짓인지(Boolean 타입) 평가 결과에따라 참일 경우 블록내의 코드를 수행하고 아닐경우 다음으로 넘어갑니다. - else if(평가식) { 코드 }
: else if는 and를 의미하는 논리연산자의 && 혹은 or을 의미하는 || 를 표현하는데 최초 조건문의 평가식이 거짓일 경우 else if의 괄호 내의 평가식을 평가합니다. 그래서 그 평가식이 참일경우 블록내의 코드를 수행하고 아닐경우 다음으로 넘어갑니다. - else { 코드 }
: else는 독립적으로는 사용하지 못하고 if문과 함께써야하는데 위에 위치한 if (혹은 else if) 조건문의 평가식이 모두 거짓일 경우 해당 블록내의 코드가 수행됩니다.
다트는 다른 언어와 달리 평가식의 결과값의 형식이 Boolean 타입만 유효합니다.
if문 을 여러번 쓰는 것과 else if를 사용하는것의 차이점
:위 코드에서 굳이 오렌지 주스인지 판단하는 것을 else if로 안하고 if로 하면 안되었을까요? 결과만 따지고 보면 위 코드에서 else if를 if로 바꿔도 에러는 발생하지 않습니다.
1.3.2 switch 와 case
내 나이를 기준으로 성인인지 미성년자인지 구분하는건 참/거짓만으로 구분할 수 있어 if문으로 충분하다. 하지만 변수에 값이 2개 이상 여러개라면 switch문을 사용하면 된다.
void main() {
Cup cup = new Cup();
cup.setDrinkType(getDrink());
switch (cup.getDrinkType()) {
case 'coffee':
print('커피입니다.');
break;
case 'orange juice':
print('오렌지 주스입니다.');
break;
default:
print("알 수 없는 음료입니다.");
}
}
- break 아니면 return 문이 있어야 switch case문을 벗어날 수 있다.
- 런타임 상수만 사용 가능하기에 평가식을 넣을 수는 없다.
switch문에서 case에 break나 return문을 사용하지 않으면 자동으로 다음 case를 실행한다.
int number = 1;
switch (number) {
case 1:
case 3:
case 5:
print("positive!");
break;
default:
print("zero!");
break;
}
위 코드를 실행하면 case 1: 에 매치되지만 break가 없기에 case 5:에서 멈춰 positive가 수행된다.
- break : 해당 switch 문을 탈출하여 다음 코드를 수행한다.
- return : 함수 실행 자체가 종료되며 함수의 반환 타입에 맞는 반환값을 넣어주지 않으면 에러가 발생한다.(void 일경우 반환타입이 없기에 반환값을 입력하지 않고 return만 작성한다.)
- throw : 오류를 의도적으로 발생시키는 키워드
- continue : 해당 레이블의 case부터 실행시키는 키워드
[실행 결과]
int number = 1;
switch (number) {
case 1:
print("1");
continue also10;
case 3:
case 5:
print("positive!");
return;
also10:
case 10:
print("10");
break;
default:
print("zero!");
break;
}
[실행 결과]
1.3.3 삼항 연산자
(평가식) ? 참일경우 : 거짓일경우
평가식의 결과가 참일 경우 물음표 뒤의 콜론을 기준으로 전자의 옵션을 반환하고, 거짓일 경우 후자의 옵션을 반환한다.
조건문을 삼항 연산자를 사용해서 좀 더 간결하게 표현할수도 있다.
//기존의 if문을 이용한 제어 흐름
if (cup.getDrinkType() == "coffee") {
print('커피입니다.');
}
//삼항 연산자를 이용한 제어 흐름
String result = cup.getDrinkType() == "coffee" ? "커피입니다" : "알 수 없는 음료입니다.";
print(result);
1.3.4 반복문
- 표준 for
- for-in
- while
- forEach
- do while
표준 for
보통 인덱스가 필요한 경우 표준 for loop를 상요하며 for문 괄호 내에 초기값을 선언해 해당 값이 반복 조건 평가식으로 평가를해서 값이 false가 될 때까지 코드를 실행하고 증감식을 수행한 뒤 다시 조건 평가를 합니다.
for (var i = 0; i < 5; i++) {
print('index: $i');
}
for - in
![](https://blog.kakaocdn.net/dn/MqIBY/btsEHpsGcQH/c6E2fx7usXIGOFJESB49Kk/img.png)
인덱스가 필요 없다면 for - in loop를 사용 할 수 있다. 이터레이션 프로토콜을 따르는 자료구조라면 모두 간단하게 사용할 수 있다.
List<String> fruits = ['apple', 'grape', 'orange', 'mango'];
for (var fruit in fruits) {
print("과일: $fruit");
}
forEach
함수형 프로그래밍으로 forEach를 사용할수도 있다. 역시 이터레이션 프로토콜을 지원하는 자료구조라면 모두 forEach를 사용할 수 있다. 그리고 또 다른 특징으로 forEach는 해당 이터레이터에서 호출하는 함수로 호출 시점에서 새로운 스코프를 만들기에 forEach에서 접근한 모든 값은 이 블록 외부에서는 접근할 수 없다.
List<String> fruits = ['apple', 'grape', 'orange', 'mango'];
fruits.forEach((fruit) => print('과일: $fruit'));
while
괄호내의 평가식이 false가 될 때까지 블록내의 코드를 수행합니다. 블록내부를 수행하기전에 조건식을 검사하기 때문에 코드가 아예 실행되지 않고 넘어갈수도 있습니다.
int count = 0;
while (count < 5) {
print(count);
count++;
}
do - while
while문과 동일하지만, while문이 평가식을 평가한 뒤에 결과에따라 블록 내의 코드를 수행한다는 점과는 다르게 do - while은 우선 do 블록내의 코드를 1회 수행 후 평가식을 평가한다는 점이 다릅니다.
int count = 0;
do {
print(count);
count++;
} while (count < 5);
반복문의 제어 흐름 키워드
break : 루프를 완전히 탈출는 키워드
for (var i = 0; i < 5; i++) {
if (i == 3) {
break;
}
print(i);
}
continue: 루프의 다음 차례로 한차례 넘기는 키워드
for (var i = 0; i < 5; i++) {
if (i == 3) {
continue;
}
print(i);
}
1.4 함수
1. 다트는 객체지향 언어이기에 함수 자체로도 객체이며 Function이라는 형식을 가진다.그렇기에 함수를 값으로 전달하거나 변수에 할당역시 가능하다. 이를 고차함수(high-order function)라 한다.
2. 다트는 화살표 함수(arrow function)을 지원한다.
![](https://blog.kakaocdn.net/dn/tfEGc/btsEFMoBdHm/gTlKvILILOLetGfGvBIdv0/img.png)
String makeGreeting(String name) => 'Hello, $name';
1.4.1 파라미터
위치 지정(positional)파라미터
void printNameAndAge(String name, int age){
print('myName is $name and my Age is $age');
}
이름 지정(named) 파라미터
void printNameAndAge({String name, int age}) {
print('myName is $name and my Age is $age');
}
...
printNameAndAge(name: 'Catsbi', age: 33);
선택형 위치 지정 파라미터
void printNameAndAge(String name, int age, [String job]) {
job = job ?? '백수';
print('myName is $name and my Age is $age and my Job is $job');
}
선택형 파라미터인 job은 선택형이기에 인수를 전달하지 않아도 오류가 발생하지 않습니다.
함수 시그니처에 = 연산자를 이용해 파라미터의 기본값을 정의할 수 있다.
void printNameAndAge(String name, int age, [String job = "백수"]) {
print('myName is $name and my Age is $age and my Job is $job');
}
1.4.2 고급 함수 개념
void printNumsPlus10(int num){
print(num + 10);
}
nums.forEach(printNumsPlus10);
1.4.3 어휘 스코프
![](https://blog.kakaocdn.net/dn/d6VLr7/btsEEFX5eh5/YGjW1RKl2eNadzMINSIMX1/img.png)
1.5 다트의 객체지향 프로그래밍
1.5.1 클래스
class TransactionEvent {
//프로퍼티와 메서드
}
다만, 다트에서는 인스턴스를 만들때 new 키워드를 사용하는 것이 선택적(Optional)이다. 컴파일러가 자동으로 알맞은 키워드를 추론하기 때문인데, 무튼 다트에서는 그렇기에 new 키워드 사용을 권장하진 않는다.
class Cat {
String name;
String color;
}
Cat pipo = Cat(); // new를 사용하지 않아도 컴파일러가 자동으로 키워드를 추론한다.
pipo.name = 'Pipo';
pipo.color = 'Yellow';
1.5.2 생성자
class Animal {
String name;
String type;
Animal(String name, String type) {
this.name = name;
this.type = type;
//추가적인 초기화 로직
}
}
다트는 여기서 더 간결하게 줄일수도 있다.
// case 1
Animal(this.name, this.type);
//case 2
Animal(this.name, this.type){
//추가적인 초기화 로직
}
1.5.3 상속
class Animal {
String name;
int legCount;
}
class Amphibian extends Animal {
String isPoisonous;
}
class Mammal extends Animal {
String furColor;
}
class Frog extends Amphibian {
void makeNoise() {
print('개굴');
}
}
class Cat extends Mammal {
void makeNoise() {
print('야옹');
}
}
class Pig extends Mammal {
void makeNoise() {
print('꿀꿀');
}
}
여기서 Cat, Pig 클래스는 Mammal 클래스를 상속받았기에 Mammal 클래스의 프로퍼티인 furColor를 이용할 수 있다. 그리고 Mammal 역시 Animal을 상속받았기 때문에 pig는 name과 legCount 프로퍼티도 이용할 수 있다. 아래 그림을 통해 한눈에 위 코드를 파악해보자.
1.5.4 factory 와 지정 생성자
미리 정해진 프로퍼티를 포함하는 클래스의 특별한 메서드
혹시라도 디자인 패턴을 공부한 적이 있다면 factory pattern에 대해 알 것이다 만약, 해당 패턴을 잘 모르지만 관심이 있다면 여기로 가서 학습할 수 있다.
지정 생성자는 항상 클래스의 새 인스턴스를 반환하며 factory 메서드는 조금 더 유연하다.
class Energy {
int joules;
//기본 생성자
Energy(this.joules);
//지정 생성자는 'Energy.'문법을 이용해 클래스의 인스턴스를 반환한다.
Energy.fromWind(int windBlows) {
final joules = _convertWindToEnergy(windBlows);
return Energy(joules);
}
//factory는 기존 Energy 인스턴스를 반환할 수 있다. 혹은 새 인스턴스를 만들어 할당한 다음 반환한다.
factory Energy.fromSolar(int sunbeams) {
if (appState.solaEnergy != null) return appState.solaEnergy;
final joules = _convertSunbeamsToJoules(sunbeams);
return appState.solarEnergy = Energy(joules);
}
}
1.5.5 열거자
상수 집합을 표현하는 특별한 클래스(enum)
enum Color { red, blue }
문자열로 색을 구분해 그에 맞는 rgb 값을 반환하는 메소드가 있다고 할 때 enum을 이용하면 형식 안정성을 확보할 수 있다.
enum Color { red, green, blue }
void updateColor(Color color) {
switch (color) {
case Color.red:
//코드
break;
case Color.blue:
//코드
break;
case Color.green:
//코드
break;
}
}
void main(){
updateColor(Color.red);
updateColor(Color.blue);
updateColor(Color.green);
}
1.6 정리
- 다트 문법은 C 언어를 기반으로 만들어진 언어와 유사하다.
- 다트는 객체지향의 엄격한 형식 언어다.
- 모든 다트 프로그램의 진입점은 main 함수다.
- 형식은 특정 상황에서 올바른 값을 할당하도록 코드를 강제한다. 이를 통해 코드의 안정성을 높힐 수 있다.
- 함수는 형식 또는 void를 반환해야 한다.
- 대부분의 연산자는 다른 연산자와 비슷하지만 ~/, is, as 와 같은 특별한 연산자도 있다.
- 어떤 값이 null인지 아닌지 확인할 때는 널 인지 연산자(null-aware)를 활용한다.
- 다트는 if/else, switch, 삼항 연산자 등의 제어 흐름을 제공한다.
- switch문에 enum을 사용하여 enum의 모든 형식을 case로 확인하도록 컴파일러가 강제한다.
- 다른 언어와 비슷한 반복문을 지원한다(for, for-in, while, do-while)
- 다트는 고차함수를 사용해 함수를 값으로 전달 혹은 반환할 수 있다.
- 기본 생성자, factory 생성자, 지정 생성자 등 다양한 생성자를 지원한다.
출처
https://catsbi.oopy.io/69bcdc20-340a-4381-bfe3-b7d8d2a3ed91
'flutter' 카테고리의 다른 글
[flutter] Flutter (1) - 개요 (0) | 2024.02.07 |
---|