중요 개념
- 모든 변수는 객체(object)이다. 모든 객체는 클래스의 인스턴스이다.
숫자, 함수, null도 객체다. 모든 객체는 Object 클래스로부터 상속된다.
- 타입 어노테이션은 타입 추론이 가능할 경우 옵션이다.
예를 들어 int number = 10;으로 명시적으로 타입을 지정하지 않고 var number = 10;으로 사용 가능하다는 의미이다.
- 타입이 예상되지 않는다고 명시적으로 표현하고 싶을 때는 dynamic 키워드를 사용한다.
이것은 하나의 변수가 여러 타입으로 변경 가능하다는 의미이다.
var 키워드와 비교해보면 좀 더 이해하기 쉽다. - var는 다음과 같이 String 타입으로 지정된 후에는 int 타입을 참조할 수 없다.
var name = "Kim"; name = 100;
: Error: A value of type 'int' can't be assigned to a variable of type 'String'. lib/main.dart:22 name = 100; ^
이때 dynamic 키워드를 사용하면 문제가 해결된다.dynamic name = "Kim"; name = 100;
- public, protected, private 키워드가 없다.
해당 라이브러리 내에 private 하려면 식별자 앞에 밑줄(_)을 붙인다.
예제 코드
main.dart
import 'package:flutter_application_223031/function.dart' as myFun;
// 함수명 앞에 리턴타입 생략이 가능
add(int a, int b) {
return a + b;
}
// 앱 실행을 시작하는 최상위 함수
main() {
// 변수 선언은 var이라는 키워드를 사용
var numA = 10;
var numB = 20;
var result = add(numA, numB);
/*
function.dart 파일내의 printRst() 함수 호출
import시 myFun를 prefix로 지정하였기 때문에 'myFun.함수명'으로 호출
*/
myFun.printRst(result);
}
function.dart
printRst(int rstNum) {
print('The number is $rstNum');
}
결과
flutter: The number is 30
식별자
변수, 함수 등의 이름
int a; // 식별자는 a
void foo(); // 식별자는 foo
내장 식별자(built-in identifier)
내장 식별자는 클래스명, 타입명, import시 prefix로 사용할 수 없다.
abstract
as
convariant
deferred
dynamic
export
external
factory
function
get
implements
import
interface
library
mixin
operator
part
set
static
typedef
키워드
특정 문맥에서 특별한 의미를 가지는 단어이다.
따라서 그 '특정 문맥'이 아닌 곳에서는 식별자로 사용 가능하다.
문맥 키워드(contextual keyword)
sync
async
hide
on
show
예약어
식별자로 사용할 수 없는 특별한 단어이다.
비동기 관련 제한된 예약어(limited reserved words related to the asynchorony support)
await
yield
기타 예약어
assert
break
case
catch
class
const
continue
default
do
else
enum
extends
false
final
finally
for
if
in
is
new
null
rethrow
return
super
switch
this
throw
true
try
var
void
while
with
주석(comment)
// 내용 : 한 줄 주석
/* 내용 */ : /*와 */ 사이의 모든 내용 주석
변수 타입
타입 | 설명 |
---|---|
num | int , double |
int | 정수 |
double | 실수 |
String | 문자열 |
bool | true , false |
var | 타입 미지정, 타입 변경 불가 |
dynamic | 타입 미지정, 타입 변경 가능 |
List | array, index 순서 있음, 중복 가능 |
Set | array, 순서 없음, 중복 불가 |
Map | key : value |
상수 타입
타입 | 설명 |
---|---|
final | 런타임시 상수화 |
const |
컴파일시 상수화 |
get() {
return 100;
}
main() {
final int NUMBER = get();
// const int PRICE = get();
// 이미 컴파일 시점에 상수화 되었기 때문에 런타임 시 get() 함수에서 가져온 값을 넣으려고 하면 에러가 발생
final NAME = 'Kim';
const COLOR = 'Red';
print('The number is $NUMBER.');
print('The name is $NAME.');
print('The color is $COLOR');
}
flutter: The number is 100.
flutter: The name is Kim.
flutter: The color is Red
함수
변수가 함수 참조 가능
타입 변수명 = 함수() { }
var name = getName() { }
다른 함수의 인자로 함수 전달 가능
함수A(함수B(), 함수C()) { }
getName(getFirstName(), getLastName()) { }
이름 있는 선택 매개변수
getAddress (String 매개변수명1, {String 매개변수명2, String 매개변수명3 }) { }
getAddress(‘seoul’, {매개변수명2: ‘gangnam’, 매개변수명3: ‘123’}) { }
위치적 선택 매개변수
함수(매개변수, [매개변수 = 초깃값, 매개변수 = 초깃값]) { }
getAddress(city, [district = '강남', zipCode = '111222']) { }
익명 함수
(매개변수명) { 표현식; };
(a, b) { a + b; };
람다식
(매개변수명) => 표현식;
(a, b) => a - b;
연산자
산술 연산자
+, -, *, /, ~/, %, ++, --
~/ : 몫의 값
% : 나머지의 값
11 ~/ 5 = 2
11 % 5 = 1
할당 연산자
=, +=, -=, *=, /=, ~/=
관계 연산자 (비교 연산자)
==, !=, >, <, >=, <=
비트 & 시프트 연산자
&, |, ^, ~, <<, >>
타입 검사 연산자
as : 형 변환 , 다른 타입으로 변환은 되지 않고 상위 타입으로 변환
is : 같은 타입이면 true
is! : 다른 타입이면 true
조건 표현식
삼항 연산자
조건 ? 표현식1 : 표현식2;
(a>0) ? '양수' : '음수';
if (a>0) {
return '양수';
} else {
return '음수';
}
조건적 멤버 접근(Conditional member access) 연산자 : null 체크
좌항?.우항
employee?.name
if (employee == null) {
return null;
} else {
return employee.name;
}
?? 연산자 : null 체크뿐만 아니라 null일 경우에 대한 처리까지
좌항 ?? 우항
employee.name ?? 'new name'
if (employee.name == null) {
return 'new name';
} else {
return employee.name;
}
캐스케이드 표기법
main() {
Employee employee = Employee()
..name = 'Kim'
..setAge(25)
..showInfo();
employee.name = 'Park';
employee.setAge(30);
employee.showInfo();
}
class Employee {
var name = 'employee';
int age = 10;
setAge(int age) {
this.age = age;
}
showInfo() {
print('$name is $age.');
}
}
Kim is 25.
Park is 30.
조건문
if, if~else
if (조건) {
실행문;
}
if (조건) {
실행문
} else { // if의 조건이 거짓이면 else의 { } 내부를 실행
실행문;
}
if(a==1) {
print('a is 1');
} else {
print('a is not 1');
}
switch
switch(변수) {
case 값1:
실행문;
break;
case 값2:
실행문;
break;
default:
실행문;
}
var number = 1;
switch(number) {
case 1: // case 값에 일치하면 해당 case 내부를 실행
print('number is 1');
break;
case 2:
print('number is 2');
break;
default: // 모든 case에 만족하지 않으면 default 내부를 실행
print('number is not 1 or 2');
}
assert
assert는 조건식이 거짓이면 에러가 발생
debug mode에서만 동작
assert(조건식);
assert(a>0);
반복문
for
for (초기화; 조건식; 증감식) {
실행문;
}
for (int i = 1; i < 5; i++) {
print('i = $i');
}
while
조건식이 참이면 내부를 순환
- 특정 조건에서 멈추고 싶다면 break를 사용
- 특정 조건에서 더 이상 다음 내용을 실행하지 않고 다시 반복하려면 continue를 사용
- 무한 반복하려면 조건식에 true
while(조건식) {
실행문;
}
int a = 0;
while(a < 5) {
print('hello');
a++;
}
do~while
한 번 실행한다(do)
이후엔 while과 동일
do {
실행문;
} while (조건식);
int a = 0;
do {
print('hello');
} while (a > 0);
클래스
기본 형태
class 클래스명 {
멤버 변수
멤버 함수
}
class Person {
String name;
getName() {
return name;
}
}
C++이나 자바에서는 객체 생성 시 new 키워드를 사용한다.
플러터는 객체 생성 시 new 키워드 기본적으로 생략
class Person {
late String name;
late int age;
getName() {
return name;
}
}
main() {
var student = new Person();
var teacher = Person();
Person person = Person();
student.name = 'Kim';
teacher.name = 'Park';
person.name = 'Unknown';
print('학생 이름이 뭐야 ? ${student.getName()}');
print('학생 이름이 뭐야 ? ${teacher.getName()}');
print('학생 이름이 뭐야 ? ${person.getName()}');
}
학생 이름이 뭐야 ? Kim
학생 이름이 뭐야 ? Park
학생 이름이 뭐야 ? Unknown
void main(){
Cars bmw = Cars(320, 100000, 'BMW');
print(bmw.price);
bmw.saleCar();
print(bmw.price);
}
class Cars {
late int maxSpeed;
late num price;
late String name;
Cars(int maxSpeed, num price, String name) {
this.maxSpeed = maxSpeed;
this.price = price;
this.name = name;
}
num saleCar(){
price = price * 0.9;
return price;
}
}
100000
90000
예) 로또번호 생성기
import 'dart:collection'; // HashSet
import 'dart:math' as math;
void main(){
var rand = math.Random();
HashSet<int> lotteryNumber = HashSet(); // Set은 중복을 허용하지 않는 리스트
while(lotteryNumber.length < 6){
lotteryNumber.add(rand.nextInt(45));
}
print(lotteryNumber);
}
{6, 9, 11, 28, 38, 42}
생성자
기본 생성자(Default constructor)
class 클래스명 {
클래스명() {
}
}
클래스를 구현할 때 생성자를 선언하지 않으면 기본 생성자가 자동으로 제공
기본 생성자는 클래스명과 동일하면서 인자가 없다. 또한 기본 생성자는 부모 클래스의 인수가 없는 생성자(=기본 생성자)를 호출한다.
기본 생성자는 상속되지 않는다.
자식 클래스는 부모 클래스의 생성자를 상속받지 않는다.
자식 클래스에서 아무 생성자도 선언하지 않으면 기본 생성자만 갖는다.
class Person {
Person() {
print('This is Person Constructor!');
}
}
class Student extends Person {
}
main() {
var student = Student();
}
This is Person Constructor!
------------------------------
Student 클래스에는 생성자가 없다.
따라서 자동으로 기본 생성자가 제공된다.
class Person {
Person() {
print('This is Person Constructor!');
}
}
class Student extends Person {
Student() {
print('This is Student Constructor!');
}
}
main() {
var student = Student();
}
This is Person Constructor!
This is Student Constructor!
------------------------------
부모 클래스(Person)의 기본 생성자가 호출된 후
자식 클래스(Student)의 기본 생성자가 호출
이름 있는 생성자(Named constructor)
생성자에 이름을 부여한 형태
class 클래스명 {
클래스명.생성자명() {
}
}
한 클래스 내에 많은 생성자를 생성하거나 생성자를 명확히 하기 위해서 사용할 수 있다.
이름 없는 생성자는 단 하나만 가질 수 있다.
또한 이름 있는 생성자를 선언하면 기본 생성자는 생략할 수 없다.
class Person {
Person() {
print('This is Person Constructor!');
}
Person.init() {
print('This is Person.init Constructor!');
}
}
class Student extends Person {
Student() {
print('This is Student Constructor!');
}
}
main() {
var student = Student();
print('---------------------');
var person = Person();
print('---------------------');
var init = Person.init();
}
This is Person Constructor!
This is Student Constructor!
---------------------
This is Person Constructor!
---------------------
This is Person.init Constructor!
초기화 리스트(Initializer list)
생성자 : 초기화 리스트 {
}
class Person {
String name;
Person() : name = 'Kim' {
print('This is Person($name) Constructor!');
}
}
main() {
var person = Person();
}
This is Person(Kim) Constructor!
리다이렉팅 생성자(Redirecting constructor)
본체가 비어있고 메인 생성자에게 위임(delegate)하는 역할
class Person {
String name;
int age;
Person(this.name, this.age) {
print('This is Person($name, $age) Constructor!');
}
Person.initName(String name) : this(name, 20);
}
main() {
var person = Person.initName('Kim');
}
This is Person(Kim, 20) Constructor!
상수 생성자(Constant constructor)
생성자를 상수처럼 만들어 줌 : 해당 클래스가 상수처럼 변하지 않는 객체를 생성
- 상수 생성자를 만들기 위해서는 인스턴스 변수가 모두 final이어야 한다.
- 생성자는 const 키워드가 붙어야 한다.
class Person {
final String name;
final num age;
const Person(this.name, this.age);
}
main() {
Person person1 = const Person('Kim', 10);
Person person2 = const Person('Kim', 20);
Person person3 = const Person('Lee', 30);
Person person4 = const Person('Kim', 10);
print(identical(person1, person2)); // 동일 인스턴스인지 비교
print(identical(person1, person3));
print(identical(person1, person4));
print(identical(person2, person3));
print(identical(person3, person4));
print('----------------');
print('person1 : ${person1.name}, ${person1.age}');
print('person2 : ${person2.name}, ${person2.age}');
print('person3 : ${person3.name}, ${person3.age}');
print('person4 : ${person4.name}, ${person4.age}');
}
false
false
true
false
false
----------------
person1 : Kim, 10
person2 : Kim, 20
person3 : Lee, 30
person4 : Kim, 10
팩토리 생성자(Factory constructor)
factory는 새로운 인스턴스를 생성하고 싶지 않을 때 사용하는 생성자
새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 factory 키워드를 사용
소프트웨어 디자인 패턴 중 '싱글톤 패턴'을 따른 것
싱글톤 패턴(singleton-pattern): 소프트웨어 디자인 패턴에서 싱글톤 패턴을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글톤 패턴이라고 한다.
- 기존에 이미 생성된 인스턴스가 있다면 return 하여 재사용한다.
- 하나의 클래스에서 하나의 인스턴스만 생성한다(싱글톤 패턴).
- 서브 클래스 인스턴스를 리턴할 때 사용할 수 있다.
- Factory constructors 에서는 this 에 접근할 수 없다.
factory는 공장이라는 이름 때문에 인스턴스를 새로 생성하는 공장이라고 착각하기 쉽다. 사실은 warehouse, 창고의 의미와 더 가깝다.
class Person {
Person.init();
factory Person(type) {
switch (type) {
case 'Student':
return Student();
case 'Employee':
return Employee();
default:
return Empty();
}
}
String getType() {
return 'Person';
}
}
class Student extends Person {
Student() : super.init();
@override
String getType() {
return 'Student';
}
}
class Employee extends Person {
Employee() : super.init();
@override
String getType() {
return 'Employee';
}
}
class Empty extends Person {
Empty() : super.init();
@override
String getType() {
return 'Empty Value';
}
}
main() {
Person student = Person('Student');
Person employee = Person('Employee');
Person anycall = Person('Anycall');
print('type = ${student.getType()}');
print('type = ${employee.getType()}');
print('type = ${anycall.getType()}');
}
type = Student
type = Employee
type = Empty Value
상속
객체지향 프로그래밍에서 상속은 클래스의 멤버를 물려주는 것
물려주는 쪽 부모 클래스(혹은 Super class), 상속을 받는 쪽 자식 클래스(혹은 Sub class)
class 부모 클래스명 {
멤버 변수;
멤버 함수() {
}
}
// 상속받는 쪽(자식 클래스)은 extends 키워드를 통해서 상속받고자 하는 부모 클래스를 지정
class 자식 클래스명 extends 부모 클래스명 {
// @override 어노테이션은 부모 클래스의 메서드를 재정의하고 싶을 때 사용
@override
멤버 함수() {
}
}
사용 목적
코드를 재사용하기 때문에 클래스가 간소화되고 수정 및 추가가 쉬워진다.
// Person 클래스는 부모 클래스
// 1개의 멤버 변수와 3개의 메서드로 구성
class Person {
late String name;
setName(String name) {
// this는 해당 인스턴스를 가리킨다
// 변수명이 동일하기 때문에 구분하기 위해서 this로 인스턴스 변수를 명확히 가리킨 것
// this.name => Person 클래스의 인스턴스 변수인 late String name
// name => setName(String name) 메서드의 인자 name
this.name = name;
}
getName() {
return name;
}
showInfo() {
print('name is $name');
}
}
// Student 클래스는 Person의 자식 클래스
// 부모 클래스인 Person의 멤버 변수와 메서드를 모두 상속
// Person의 멤버 변수와 메서드를 별도로 Student 클래스 내에 선언하지 않아도 사용 가능
class Student extends Person {
late int studentID;
// 부모 클래스 함수의 기능을 변경하고 싶다면 재정의
@override
showInfo() {
// super는 부모 클래스를 가리킨다
print('1. name is ${super.getName()} and id is $studentID.');
// 자식 클래스는 이미 부모 클래스의 getName() 메서드도 상속받았기 때문에 생략 가능
print('2. name is ${getName()} and id is $studentID.');
// name 역시 상속받았기 때문에 name변수를 사용 가능
print('3. name is $name and id is $studentID.');
}
}
main() {
Person person = Person();
person.setName('Kim');
person.showInfo();
print('-----------');
Student student = Student();
student.studentID = 2020;
student.setName('Kim');
student.showInfo();
}
name is Kim
-----------
1. name is Kim and id is 2020.
2. name is Kim and id is 2020.
3. name is Kim and id is 2020.
접근 지정자
private, public
다트의 접근 지정자 접근 범위
- private : 해당 라이브러리 내에서만 접근 가능 (import 후 접근 불가)
- public : 제한없이 접근 가능
기본적으로 아무런 키워드가 없을 경우 public
private로 선언하기 위해서는 변수나 메서드 앞에 _(밑줄)
class Person {
String name; // public
int _age; // private
eat() { // public
print('eat');
}
_sleep() { // private
print('sleep');
}
}
Getter, Setter
캡슐화를 통해 클래스의 내부 정보를 공개하지 않도록 함
멤버 변수를 private로 선언하고 해당 변수에 접근할 수 있는 메서드는 public으로 선언하면 멤버 변수에 직접적으로 접근하는 것을 막을 수 있다.
- Getter : 멤버 변수의 값을 가져오는 역할
- Setter : 멤버 변수의 값을 쓰는 역할
class Person {
late String _name;
String get name => _name; // Getter
// set name의 name은 임의로 지정해준 이름
set name(String name) => _name = name; // Setter
set setName(String name) => _name = name; // Setter
}
main() {
Person person = Person();
person.name = 'Kim';
print(person.name);
person.setName = 'Kevin';
print(person.name);
}
Kim
Kevin
추상 클래스
추상 메서드를 가질 수 있는 클래스
메서드 선언은 되어 있지만 바디가 정의되지 않은 형태
// 기존 클래스 앞에 추상이란 뜻을 가진 abstract 키워드를 붙여서 표현
abstract class Person {
eat();
}
- 추상 클래스는 미완성 클래스이기 때문에 객체를 생성할 수 없다
- 참조형 변수의 타입으로는 사용이 가능
- 추상 클래스를 사용하기 위해서는 일반 클래스에서 implements 키워드로 임플리먼트 한 후에 반드시 추상 메서드를 오버라이딩 해야 한다.
// 추상 클래스
abstract class Person {
eat();
sleep() {
print('Person must sleep');
}
}
abstract class Junior {
work() {
print('work hard');
}
}
// 추상 클래스인 Person를 임플리먼트
class Developer implements Person, Junior {
// 꼭 다른 기능으로 사용하기 위한 재정의가 아니더라도
// 추상 클래스 Person의 추상 메서드인 eat()를 오버라이딩
@override
eat() {
print('Developer like iced Americano.');
}
// @override를 생략가능
sleep() {
print('Developer must sleep');
}
@override
work() {
print('Junior developer works hard');
}
}
main() {
// person 객체의 타입으로 추상 클래스인 Person을 사용
// 실제 생성되는 객체는 Developer
Developer person = Developer();
person.eat();
person.sleep();
person.work();
//Person person = Person(); // 추상 클래스로 객체를 생성하려고 하면 에러가 발생
}
Developer like iced Americano.
Developer must sleep
Junior developer works hard
컬렉션
- List : 데이터 순서가 있고 중복 허용
- Set : 데이터 순서가 없고 중복 허용하지 않음
- Map : 키(key)와 값(value)으로 구성되며 키는 중복되지 않고 값은 중복 가능
List
데이터 순서가 있고 중복 허용
- 데이터를 여러 개 담을 수 있는 자료구조
- 데이터를 List에 담을 때 순서를 가지기 때문에 배열을 대체할 수 있고 데이터에 순차적으로 접근하기 쉽다
List<데이터 타입> 변수명 = [데이터1, 데이터2, 데이터3, ...];
List<String> colors = ['Red', 'Orange', 'Yellow'];
List<데이터 타입> 변수명 = List();
colors .add(데이터1);
colors .add(데이터2);
colors .add(데이터3);
List<String> colors = List();
colors .add('Red');
colors .add('Orange');
colors .add('Yellow');
주요 메서드와 프로퍼티
- indexOf(요소) : 요소의 인덱스 값
- add(데이터) : 데이터 추가
- addAll([데이터1, 데이터2]) : 여러 데이터 추가
- remove(요소) : 요소 삭제
- removeAt(인덱스) : 지정한 인덱스의 요소 삭제
- contains(요소) : 요소가 포함되었으면 true, 아니면 false
- clear() : 리스트 요소 전체 삭제
- sort() : 리스트 요소 정렬
- first : 리스트 첫 번째 요소
- last : 리스트 마지막 요소
- reversed : 리스트 요소 역순
- isNotEmpty : 리스트가 비어있지 않으면 true, 비었으면 false
- isEmpty : 리스트가 비었으면 true, 비어있지 않으면 false
- single : 리스트에 단 1개의 요소만 있다면 해당 요소 리턴
예제코드
main() {
List<dynamic> list = [1, 2.5, 'test'];
print('index of test = ${list.indexOf('test')}'); // test 인덱스 값
list.add('신규'); // 한개의 요소 추가
list.addAll([100, 'Korea']); // 여러 요소 추가
list.remove(2.5); // 요소(2.5) 삭제
list.removeAt(0); // 인덱스(0) 요소 삭제
print(' ---------- 목록의 시작 -----');
for (int i = 0; i < list.length; i++) {
print('리스트${i}: ${list[i]}');
}
print(' ---------- 목록의 끝 -----');
print('목록 처음 = ${list.first}');
print('목록 마지막 = ${list.last}');
print('목록 역순 = ${list.reversed}');
if (list.contains('new')) {
print('신규가 있습니다. ');
}
if (list.isNotEmpty) {
print('리스트는 비어 있지 않습니다');
}
list.clear(); // 리스트 모든 항목 삭제
for (int i = 0; i < list.length; i++) { // 비어 있으므로 출력 없음
print('리스트${i}: ${list[i]}');
}
if (list.isEmpty) {
print('리스트가 비어 있습니다');
}
list.add(1000);
// 리스트의 요소가 1개라면 해당 요소 리턴
print('한개의 요소 값 = ${list.single}');
list.addAll([100, 20, 1, 200, 5, 3, 30, 20001]);
list.sort(); // 리스트 정렬
print(list);
}
index of test = 2
---------- 목록의 시작 -----
리스트0: test
리스트1: 신규
리스트2: 100
리스트3: Korea
---------- 목록의 끝 -----
목록 처음 = test
목록 마지막 = Korea
목록 역순 = (Korea, 100, 신규, test)
리스트는 비어 있지 않습니다
리스트가 비어 있습니다
한개의 요소 값 = 1000
[1, 3, 5, 20, 30, 100, 200, 1000, 20001]
Set
List와 동일하지만,
-
데이터 순서가 없고 중복 불가
- List는 [ ]를 사용했지만 Set은 { }를 사용
Set<데이터 타입> 변수명 = {데이터1, 데이터2, 데이터3, ...};
Set<String> colors = {'RED', 'Orange', 'Yellow'};
Set<데이터 타입> 변수명 = Set();
colors .add(데이터1);
colors .add(데이터2);
colors .add(데이터3);
Set<String> colors = Set();
colors .add('Red');
colors .add('Orange');
colors .add('Yellow');
주요 메서드와 프로퍼티 (인덱스와 관련된것 사용 불가)
indexOf(요소) : 요소의 인덱스 값- add(데이터) : 데이터 추가
- addAll([데이터1, 데이터2]) : 여러 데이터 추가
- remove(요소) : 요소 삭제
removeAt(인덱스) : 지정한 인덱스의 요소 삭제- contains(요소) : 요소가 포함되었으면 true, 아니면 false
- clear() : 리스트 요소 전체 삭제
sort() : 리스트 요소 정렬- first : 리스트 첫 번째 요소
- last : 리스트 마지막 요소
reversed : 리스트 요소 역순- isNotEmpty : 리스트가 비어있지 않으면 true, 비었으면 false
- isEmpty : 리스트가 비었으면 true, 비어있지 않으면 false
- single : 리스트에 단 1개의 요소만 있다면 해당 요소 리턴
예제코드
main() {
Set<dynamic> list = {1, 2.5, 'test'};
//print('index of test = ${list.indexOf('test')}'); // Set에서 사용불가
list.add('신규'); // 한개의 요소 추가
list.addAll([100, 'Korea']); // 여러 요소 추가
list.remove(2.5); // 요소(2.5) 삭제
//list.removeAt(0); // Set에서 사용불가
print(' ---------- 목록의 시작 -----');
for (int i = 0; i < list.length; i++) {
// 비어 있으므로 출력 없음
// print(list[i]); // Set에서 index 사용불가
}
print(list);
print(' ---------- 목록의 끝 -----');
print('목록 처음 = ${list.first}');
print('목록 마지막 = ${list.last}');
//print('목록 역순 = ${list.reversed}'); // Set에서 사용불가
if (list.contains('new')) {
print('신규가 있습니다. ');
}
if (list.isNotEmpty) {
print('리스트는 비어 있지 않습니다');
}
list.clear(); // 리스트 모든 항목 삭제
if (list.isEmpty) {
print('리스트가 비어 있습니다');
}
list.add(1000);
// 리스트의 요소가 1개라면 해당 요소 리턴
print('한개의 요소 값 = ${list.single}');
list.addAll([100, 20, 1, 200, 5, 3, 30, 20001]);
//list.sort(); // Set에서 사용불가
print(list);
}
---------- 목록의 시작 -----
{1, test, 신규, 100, Korea}
---------- 목록의 끝 -----
목록 처음 = 1
목록 마지막 = Korea
리스트는 비어 있지 않습니다
리스트가 비어 있습니다
한개의 요소 값 = 1000
{1000, 100, 20, 1, 200, 5, 3, 30, 20001}
Map
Key 와 Value로 이루어짐
-
키와 값은 한 쌍
- 키에 대한 값이 매칭 되어 있어서 빠른 탐색이 가능
Map<키 타입, 값 타입> 변수명 = {
키1:값1,
키2:값2,
키3:키3
};
Map<int, String> testMap = {
1:'Red',
2:'Orange',
3:'Yellow'
};
Map<키 타입, 값 타입> 변수명 = Map();
변수명[1] = 값1;
변수명[2] = 값2;
변수명[3] = 값3;
Map<int, String> testMap = Map();
testMap[1] = 'Red';
testMap[2] = 'Orange';
testMap[3] = 'Yellow';
주요 메서드
- update() : 키에 대한 값의 맵핑을 새로운 값으로 변경
예제코드
main() {
Map<int, String> testMap = {1: 'red', 2: 'orange', 3: 'yellow'};
testMap[4] = 'green';
testMap.update(1, (value) => 'newRed', ifAbsent: () => 'newColor');
testMap.update(5, (value) => 'newBlue', ifAbsent: () => 'newColor');
print(testMap[1]);
print(testMap[5]);
print(testMap);
}
newRed
newColor
{1: newRed, 2: orange, 3: yellow, 4: green, 5: newColor}
제네릭(Generic)
타입 매개변수를 통해 다양한 타입에 대한 유연한 대처를 가능하게 한다
< > 에 타입 매개변수를 선언하는 것을 매개변수화 타입(Parameterized type)을 정의한다고 한다.
타입 매개변수
매개변수는 클래스 생성 시 생성자에서 사용하거나 함수 호출 시 인자 값을 전달하기 위해 사용
전달하는 것이 인자 값이 아니라 타입
class Person {
eat() {
print('Person eats a food');
}
}
// Person의 자식 클래스
class Student extends Person {
eat() {
print('Student eats a hambuger');
}
}
// <T extends Person> 타입 매개변수
// Person 클래스와 그 자식 클래스가 실제 타입 매개변수
class Manager<T extends Person> {
eat() {
print('Manager eats a sandwich');
}
}
class Dog {
eat() {
print('Dog eats a dog food');
}
}
void main() {
var manager1 = Manager();
manager1.eat();
var manager2 = Manager<Person>();
manager2.eat();
var manager3 = Manager<Student>();
manager3.eat();
print('--------------');
var dog = Dog();
dog.eat();
/*
Dog 클래스는 Manager 클래스의 실제 타입 매개변수가 아니다
var manager4 = Manager<Dog>(); // 에러
manager4.eat();
*/
}
Manager eats a sandwich
Manager eats a sandwich
Manager eats a sandwich
--------------
Dog eats a dog food
제네릭 메서드
제네릭은 메서드에도 사용할 수 있다.
메서드의 리턴 타입, 매개변수를 제네릭으로 지정할 수 있다.
class Person {
T getName<T>(T param) {
return param;
}
}
void main() {
var person = Person();
print(person.getName('Kim'));
print(person.getName<String>('Kevin'));
print(person.getName(123));
//print(person.getName<String>(4567)); // 에러
print(person.getName<int>(4567));
//print(person.getName<int>('Bunny')); // 에러
}
Kim
Kevin
123
// 에러
4567
// 에러
printValue <T> (T aNumber){
print('is $aNumber');
}
main(){
var number1= 'two';
printValue<String>(number1);
var number2= 2;
printValue<int>(number2);
}
is two
is 2
비동기 프로그래밍
요청한 작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어감으로써 프로그램의 실행을 멈추지 않는다
isolate
싱글 스레드를 가지고 있고 이벤트 루프를 통해 작업을 처리
기본 isolate인 main isolate는 런타임에 생성
isolate 구조 및 기존 스레드와 차이점
자바 등의 다른 언어에서 사용하는 스레드는 다음과 같이 스레드가 서로 메모리를 공유하는 구조
- 하지만 isolate의 스레드는 자체적으로 메모리를 가지고 있고 메모리 공유가 되지 않는다.
- isolate는 서로 분리된 작은 공간들이고, 둘을 함께 작동시키려면 메시지를 서로 전달하도록 해야함
- Dart에서는 스레드가 메모리를 가진채로 isolate에 있고 이벤트만 처리
새로운 isolate 생성
새로운 isolate는 해당 메모리에서 고유 이벤트 루프를 가지게 된다.
이벤트 루프 - 이벤트 큐에서 가장 오래된 이벤트를 처리하고 그다음으로 넘어가서 처리하면서 큐가 비어질때까지 동작한다.
isolate 간 message 주고받기
message를 보낼 때는 SendPort의 send를 이용하고 수신할 때는 ReceivePort의 listen을 이용
import 'dart:isolate';
isolateTest(var m) {
print('isolate no. $m');
}
Future<void> main() async {
isolateTest(0);
// ReceivePort는 isolate 간에 message를 주고받을 수 있는 역할
ReceivePort mainReceivePort = new ReceivePort();
var test1 = Isolate.spawn(isolateTest, 1);
var test2 = Isolate.spawn(isolateTest, 2);
var test3 = Isolate.spawn(isolateTest, 3);
isolateTest(4);
Isolate? isolate;
isolate = await Isolate.spawn<String>((message) {
isolateTest(5);
}, 'message');
}
isolate no. 0
isolate no. 4
isolate no. 1
isolate no. 2
isolate no. 3
isolate no. 5
import 'dart:isolate';
main() {
int counter = 0;
// main isolate에서 사용할 ReceivePort인 mainReceiverPort를 생성
ReceivePort mainReceivePort = new ReceivePort();
// mainReceiverPort에서 message를 수신하는 listen를 선언
mainReceivePort.listen((fooSendPort) {
if (fooSendPort is SendPort) {
// 수신한 message가 SendPort 타입이면 해당 SendPort로 count 변수를 message로 하여 send
fooSendPort.send(counter++);
} else {
// 수신한 message가 SendPort 타입이 아니면 message를 출력
print(fooSendPort);
}
});
// 5개의 isolate를 생성
for (var i = 0; i < 5; i++) {
Isolate.spawn(foo, mainReceivePort.sendPort);
}
}
foo(SendPort mainSendPort) {
// 새로 생성된 isolate의 RecivePort인 fooReceivePort를 생성
ReceivePort fooReceivePort = new ReceivePort();
// isolate 생성 시 전달받은 main isolate의 SendPort를 이용하여 main isolate에 새로 생성된 isolate의 SendPort를 전달
mainSendPort.send(fooReceivePort.sendPort);
fooReceivePort.listen((message) {
mainSendPort.send('received: $message');
});
}
received: 0
received: 1
received: 2
received: 3
received: 4
future, async, await
future
작업이 완료되면 결과를 받는 방식으로 비동기 처리, 요청한 작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어간다.
- Uncompleted(미완료) : future 객체를 만들어서 작업을 요청한 상태
- Completed(완료) : 요청한 작업이 완료된 상태
- data : 정상적으로 작업을 수행하여 결괏값을 리턴하며 완료
- error : 작업 처리 중 문제 발생 시 에러와 함께 완료
event loop에 의해서 순차적으로 처리
main() {
// (1) start를 출력하는 작업이 event queue에 가장 먼저 들어가서 처음으로 실행
print('start');
// future 객체를 만들 때 타입은 Future<T>와 같이 제네릭을 사용
// Uncompleted future 상태
// (2) Uncompleted future가 event queue에 들어간다
Future<String> myFuture = Future(() {
for (int i = 0; i < 100000000; i++) {
// loop
}
//return 'I got lots of data!';
return throw Exception('Failed : data is too many');
});
// future의 작업이 완료되면 then()이 호출
// Completed future 상태
myFuture.then((data) {
print(data);
}, onError: (e) {
print(e);
});
print('do something');
}
start
do something
Exception: Failed : data is too many
main() {
print('start');
var myFuture = getData();
myFuture.then((data) => test(data)).catchError((e) => print(e));
// catchError는 test() 함수에서 발생한 에러를 처리하여 Exception을 출력
var myFuture2 = getData();
myFuture2.then((data) {
test(data);
}, onError: (e) {
// onError는 에러를 처리하지 못함
print(e);
});
print('end');
}
Future<String> getData() {
return Future(() {
return 'return message';
});
}
test(String data) {
print(data);
return throw Exception('Failed : test is empty');
}
아래 코드 주석 처리한 결과
var myFuture2 = getData();
myFuture2.then((data) {
test(data);
}, onError: (e) {
print(e);
});
-------------
start
end
return message
Exception: Failed : test is empty
async, await
비동기 함수 내에서 await가 붙은 작업은 해당 작업이 끝날 때까지 다음 작업으로 넘어가지 않고 기다린다.
- async와 await는 한 쌍으로 사용
함수명() async {
await 작업함수();
}
-
함수 이름 뒤, 본문이 시작하는 중괄호 { 앞에 async 키워드를 붙여 비동기로 만든다.
-
비동기 함수 안에서 언제 끝날지 모르는 작업 앞에 await 키워드를 붙인다.
-
2번 작업을 마친 결과를 받기 위해 비동기 함수 이름 앞에 Future(값이 여러 개면 Stream) 클래스를 지 정한다.
async 미적용
main() {
print('start');
var myFuture = getData();
print('result : $myFuture');
print('end');
}
Future<String> getData() {
var test = Future(() {
for (int i = 0; i < 100000000; i++) {
//
}
return 'too many time';
});
return test;
}
start
result : Instance of 'Future<String>'
end
async 적용
main() async {
print('start');
var myFuture = await getData();
print('result : $myFuture');
// 또는 아래 코드
await getData().then((value) => {
print(value)
});
print('end');
}
Future<String> getData() {
var test = Future(() {
for (int i = 0; i < 100000000; i++) {
//
}
return 'too many time';
});
return test;
}
start
result : too many time
end
stream
- 연속된 데이터를 listen()을 통해서 비동기적으로 처리 (동영상 처리에 사용)
- 실시간으로 서버를 살펴보다가 서버에서 데이터가 변경되면 화면을 새로 고침하지 않더라도 자동으로 변경된 데이터가 반영되어야 할 때 사용
main() async {
print('start');
var stream = Stream.value(100).listen((dynamic x) => print('getData : $x'));
print('end');
}
start
end
getData : 100
main() async {
print('start');
// Stream.periodic()은 특정 주기로 반복적으로 이벤트를 발생
//var stream = Stream.periodic(Duration(seconds: 1), (x) => x).take(5);
//stream.listen(print);
var stream = Stream.periodic(Duration(seconds: 1), (x) => x).take(5).listen(print);
print('end');
}
start
end
0
1
2
3
4
main() async {
print('start');
var stream = Stream.periodic(Duration(seconds: 1), (x) => x + 1)
.take(5)
.listen((x) => print('periodic : $x'));
Stream.fromIterable(['1', 2, 3, 4, '5'])
.listen((dynamic x) => print('fromIterable : $x'));
Stream.fromFuture(getData()).listen((x) => print('fromFuture : $x'));
print('end');
}
Future<String> getData() async {
return Future.delayed(Duration(seconds: 3), () => '3초 후');
}
start
end
fromIterable : 1
fromIterable : 2
fromIterable : 3
fromIterable : 4
fromIterable : 5
periodic : 1
periodic : 2
periodic : 3
fromFuture : 3초 후
periodic : 4
periodic : 5