티스토리 뷰

공부한 내용 정리/Flutter

Dart 기초

ProWiseman 2023. 7. 2. 19:37

들어가며

본 글은 노마드 코더의 Dart 시작하기를 보고 정리한 글입니다. 노마드 코더에서 Flutter를 시작하기 앞서 Dart를 공부하는게 중요하다고 강조하기에 Dart를 공부하고 본 포스트에 정리하고자 합니다.

 

Flutter가 Dart를 채택한 이유

1. Dart는 JIT 컴파일과 AOT 컴파일이 둘 다 있다. (개발시 빠른 피드백 + 배포시 빠른 실행속도)

2. Dart와 Flutter 모두 구글에서 만들었다. (구글에서 Dart 자체를 Flutter를 위해 최적화 가능)

 

Dart는 Dart Web, Dart Native 두 개의 컴파일러를 가지고 있다.

 

Dart Web은 Dart 코드를 Javascript로 변환해주는 컴파일러이다.

Dart Native는 Dart 코드를 여러 CPU 아키텍쳐에 맞게 변환해준다. 이 덕분에 Dart만으로 IOS, Android, Windows, Linux, Mac 등 여러 환경에서 컴파일 할 수 있다.

 

Dart Native는 Dart를 JIT(just-in-time)과 AOT(ahead-of-time)로 컴파일 된다. AOT는 C/C++나 GO와 같이 실행 전 각 환경에 맞게 바이트코드를 바이너리로 컴파일하는 것을 의미한다. 그러나 특정 아키텍쳐에 대해 AOT 컴파일링을 한다면 시간이 너무 오래 걸리기 때문에 UI같이 계속 변경하고 확인해 주어야 하는 것을 만들 때는 효과적인 방법이 아니다.

 

따라서 Dart 개발 중엔 Dart VM을 통해 JIT 컴파일을 한다. 이를 통해 코드의 결과를 바로 확인할 수 있지만 가상 머신이기에 조금 느리다. 그렇기에 개발이 끝나고 배포할 때는 AOT 컴파일러를 이용한다.

 

Dart는 null safety를 제공하여 안전한 프로그램 빌드를 하도록 한다. (Flutter 강좌에서 더 설명한다 했음)

 

 

main 함수

main 함수는 모든 Dart 프로그램의 Entry point이다. main에서 사용자가 쓴 코드가 호출된다. 만약 main 함수가 없다면 에러를 출력한다.

void main() {
  print("Hello World!");
}

 

VARIABLES

var

가장 기본적인 변수 생성 방법은 var을 이용하는 것이다. Dart 컴파일러가 변수가 어떤 타입인지 알아서 추측하기 때문에 구체화할 필요는 없다.

void main() {
  var name = "김영석";
}

단, 값을 업데이트 할 때는 변수의 본래 타임과 일치하여야 한다. 가령 위 name에 1을 대입하려 한다면 다음의 에러를 출력한다.

void main() {
  var name = "김영석";
  name = 1;
}

Error: A value of type 'int' can't be assigned to a variable of type 'String'.

 

변수 타입 명시적 지정

또 다른 변수 생성 방법으론 명시적으로 변수 타입을 지정하는 것이다. 다음 코드는 var 대신 String을 통해 명시적으로 변수형을 지정한 예시이다.

void main() {
  String name = "김영석";
}

 

관습적으로 함수나 메소드 내부에 지역 변수를 선언할 때는 var를 사용하고 Class에서 변수나 Property를 선언할 때는 타입을 지정한다.

 

Dynamic Type

아래 코드와 같이 var로 변수를 선언만 하거나 dynamic이라 명시하면 dynamic 타입이 선언이 된다.dynamic 변수에는 아무 타입의 변수를 넣어도 된다. 따라서 정확히 어떤 타입의 값이 넘어올 지 모르는 상황에 유용하다.

void main() {
  var name; // dynamic name;
  name = "김영석";
  name = 12;
  name = true;
}

 

dynamic 타입은 dart가 변수의 타입을 모르기 때문에 사용할 수 있는 메소드가 제한적이다. 그러나 아래 코드와 같이 특정 타입이라는 확신이 있으면 그 타입에 대한 메소드를 사용할 수 있다.

void main() {
  dynamic name;
  if(name is String) {
    // code
  }
}

사용할 수 있는 메소드가 다르다.

다만 dynamic 형을 쓰는 것은 지양해야 한다.

 

Nullable Variables

Dart는 null safety를 제공한다. null safety는 개발자가 null 값을 참조할 수 없도록 하는 것이다. 보통의 언어의 경우 만약 코드에서 null 값을 참조한다면 런타임 에러를 반환한다. 이상적으로 컴파일 전에 이 에러를 잡아내는 것이 좋다. 그게 null safety가 하는 일이다.

 

다음의 코드의 경우 null safety를 적용하지 않은 코드이며 null에는 존재하지 않는 메소드에 접근하기 때문에 NoSuchMethodError를 반환한다.

// without null safety:
bool isEmpty(String string) => string.length == 0;

void main() {
  isEmpty(null); 
}

 

Dart의 경우 보통 다른 변수 타입에 null 값을 대입하진 못한다.

 

void main() {
  String name = "영석";
  name = null;
}

Error: The value 'null' can't be assigned to a variable of type 'String' because 'String' is not nullable.

 

그러나 변수 타입 뒤에 ?를 붙여주게 되면 null을 대입할 수 있다.

void main() {
  String? name = "영석";
  name = null;
}

이제 컴파일러가 변수가 null이 될 수 있다는 사실을 알기 때문에 여러 유용한 도움을 준다.

예를 들어 name.length를 하게 되면 컴파일러가 name이 null일 수도 있다는 사실을 알려준다.

다음과 같이 변수가 null이 아니라는 조건문을 제공하면 컴파일러는 더 이상 메시지를 반환하지 않는다.

매번 조건문 검사를 다 치려면 시간이 오래 걸리기 때문에 다른 간단한 표현법도 있다.

if (name != null) {
  name.length;
}
// 다음으로 축약
name?.length;

 

 

Final Variables

기존의 변수는 선언 이후에 지속적으로 수정할 수 있다. 그러나 final을 사용하면 한 번 정의된 변수를 수정할 수 없도록 할 수 있다. javascript나 typescript의 const와 같은 기능을 한다.

void main() {
  final name = "영석"; // 어떤 타입의 변수일지 명시하는 것은 필수가 아니다.
  final int age = 21 // 다만 원한다면 변수 타입을 명시해 줄 수 있다.
}

 

Late Variables

late는 final이나 var 앞에 붙여줄 수 있는 수식어이다. late는 초기 데이터 없이 변수를 선언할 수 있도록 한다. 즉 먼저 선언하고 나중에 변수를 넣어준다.

void main() {
  late final String name;
  // do something, go to api
  name = "영석";
}

late를 사용하면 값이 할당되기 전까진 해당 변수에 접근할 수 없도록 한다.

 

late를 쓰지 않고도 나중에 변수에 값을 할당해줄 수 있는데 왜 late를 쓰는지 잘 이해가 가지 않아 댓글을 살펴보니 잘 설명한게 있어 첨부한다.

추후에 배울 class에서 이 키워드가 쓰입니다.

class 내의 인스턴스 변수가 final이면 만들면서 바로 할당해야하고
late final이면 만들고 난 후에 할당해도 됩니다.

또한 Nullable 타입이 아닌(?가 없는) 타입들은 빈 값(null)을 가질 수 없는데 late 키워드를 써줘서 사용하기 전에 할당한다고 알려줄 수 있죠.
즉, null-satefy가 보장됩니다.

결론은 지금 한 함수 내에서 쓰는건 별 의미가 없습니다.
다른 함수에서 값을 변경할 수 있을 때 의미가 생깁니다.

 

Constant Variables

Dart에서의 const는 javascript나 typescript에서의 그것과는 다르다. 위에서 설명했듯 javascript나 typescript에서의 const는 final과 더 유사하다. Dart에서의 const는 compile-time constant를 만들어준다.

 

이는 compile-time에 해당 변수가 무엇인지 알아야 한다는 것을 의미한다. API로부터 값을 요청하는 경우를 생각해보자. 다음의 경우 컴파일러는 API의 값을 모르기 때문에 에러를 반환한다. 단 final은 아니다.

void main() {
  const API = fetchApi();
}

 

 

DATA TYPES

Basic Data Types

기본 자료형엔 String ,bool, int, double, num이 있으며 이들은 object로 이루어져 있다. 이 중 int와 double은 num을 상속받는다. num 자료형을 사용하면 int와 double 모두 사용할 수 있다.

void main() {
  String name = "영석";
  bool alive = true;
  int age = 12;
  double money = 69.99;
  
  num x = 12;
  x = 1.1;
}

모든 자료형이 object로 구현되어 있기 때문에 실제로 자료형 안에 어떤게 들어있는지 볼 수 있다.

 

Lists

리스트를 선언하는 방법은 다음과 같다.

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

리스트에 요소를 추가할 때는 다른 기본 자료형과 마찬가지로 동일한 타입의 요소만 추가할 수 있다.

void main() {
  List<int> numbers = [1, 2, 3, 4, 5];
  numbers.add(1);
}

 

Dart에서의 리스트가 특별한 점은 collection if와 collection for을 지원한다는 점이다. 

 

다음 코드는 collection if를 활용하는 코드이다. giveMeFive가 true이면 해당 요소를 리스트에 입력한다.

void main() {
  var giveMeFive = true;
  var numbers = [
    1,
    2,
    3,
    4,
    if (giveMeFive) 5,
  ];
}

 

그 다음 collection for이다. 다만 collection for을 이해하기 위해선 string interpolation에 대한 이해가 선행되어야 한다.

String Interpolation

String interpolation은 텍스트에 변수를 추가하는 방법이다. 다음의 코드와 같이 문자열 내에 $변수명 을 해주면 된다.

void main() {
  var name = "영석";
  var greeting = "Hello eveyone, my name is $name, nice to meet you!";
  print(greeting);
}

>> Hello eveyone, my name is 영석, nice to meet you!

 

이미 변수 선언된 변수를 가져올 때 외에도 계산을 할 때도 사용이 가능한데, 이 경우엔 문법이 약간 다르다. ${변수명과 계산} 을 해주면 된다.

void main() {
  var name = "영석";
  var age = 19;
  var greeting = "Hello eveyone, my name is $name and I'm ${age+2}";
  print(greeting);
}

>> Hello eveyone, my name is 영석 and I'm 21

 

다음 코드는 String interpolation과 collection for을 이용한 코드이다.

void main() {
  var oldFriends = ["영석", "철수"];
  var newFriends = [
    "영희",
    "길동",
    for (var friend in oldFriends) "❤ $friend",
  ];
  print(newFriends);
}

>> [영희, 길동, ❤ 영석, ❤ 철수]

 

Maps

Map은 Javascript나 Typescript의 object, Python의 Dictionary같은 것이다.

 

선언 방법은 다음과 같다.

void main() {
  var player = {
    "name": "영석",
    "xp": 19.99,
    "superpower": false,
  };
}

여기서 player의 타입은 Map<String, Object>이다. 이는 key가 String, value는 Object라는 의미이다. Dart는 모든 게 object로부터 생기기 때문에 object는 어떠한 자료형이든 될 수 있다. 따라서 Object는 Typescript의 any로 보면 된다.

 

Dart에서는 모든 것이 class이기 때문에 모든 자료형은 property 또한 갖는다. 다음은 Map에서의 예시이다.

다만 니콜라스는 key value를 가지는 구조로 object를 만들 땐 Map보단 class로 만드는 것을 선호한다고 한다.

 

Sets

Set과 List의 차이점은 Set에 속한 모든 아이템들은 유일하다는 점이다.

 

다음 코드는 선언과 Set에 이미 존재하는 요소를 여러번 추가했을 때의 실행 결과를 살펴보는 코드이다.

void main() {
  // 선언
  var numbers = {1, 2, 3, 4, 5,};
//   Set<int> numbers = {1, 2, 3, 4, 5,};
  
  // 실행
  numbers.add(1);
  numbers.add(1);
  numbers.add(1);
  print(numbers);
}

>> {1, 2, 3, 4, 5}

이미 존재하는 요소는 여러번 추가해도 그대로이다. 추가로 Set은 List와 마찬가지로 sequence이므로 순서를 갖는다.

 

FUNCTIONS

함수 선언 방법

다음 코드를 살펴보며 Dart에서의 함수를 살펴보자. 함수의 앞에는 C/C++나 Java같은 여타 다른 언어들과 마찬가지로 반환형이 오게 된다. void는 반환값이 없다는 의미이고, String은 String을 반환한다는 의미이다.

void sayHello(String name) {
  print("Hello $name nice to meet you!!");
}

String sayHello(String name) {
  return "Hello $name nice to meet you!!";
}

void main() {
  print(sayHello('영석'));
}

Fat Arrow Syntax

앞서 살펴본 함수 선언은 더 간소화 할 수 있다. 다음 코드처럼 화살표(=>)를 통해 바로 return 할 수 있다. 이는 한 줄짜리같은 간단한 함수일 때 유용하다.

String sayHello(String name) => "Hello $name nice to meet you!!";

num plus(num a, num b) => a + b;

 

Named Parameters

다음 코드에서 main을 살펴보면 인수가 여러개인 함수에서 각 인수가 어떤 것을 의미하는지 불분명할 수 있다. 이는 인수를 순서대로 전달해주기 때문에 Positional Parameter라고 한다.

String sayHello(String name, int age, String country) {
  return "Hello $name, you are $age, and you com from $country";
}

void main() {
  print(sayHello('영석', 21, '대한민국'));
}

이를 해소하기 위해 Named Parameters와 Named Arguments를 적용할 수 있다. 이 경우엔 함수를 호출할 때 인수의 순서는 더 이상 중요하지 않고 이름에 따라 인수를 받게 된다.

 

Named Parameters는 단순히 파라미터에 중괄호를 쳐주면 된다. Named Arguments는 main에 보이는 것 처럼 변수 이름 뒤에 콜론과 값을 붙여주면 된다.

String sayHello({String name, int age, String country}) {
  return "Hello $name, you are $age, and you com from $country";
}

void main() {
  print(sayHello(
    age: 21,
    country: '대한민국',
    name: '영석'
  ));
}

그러나 이 경우 null safety의 문제가 발생한다. sayHello 함수에선 모든 변수를 사용하지만 함수를 사용할 때 모든 인수가 전해지리란 보장이 없기 때문이다. 이를 해결하기 위한 방법은 몇 가지가 있다.

 

그 중 하나는 named argument에 default value를 정하는 것이다. 인수가 전달되지 않을 경우 default value가 전달된다.

String sayHello({
  String name = 'anon', 
  int age = 99, 
  String country = 'wakanda'
}) {
  return "Hello $name, you are $age, and you com from $country";
}

void main() {
  print(sayHello(
    age: 21
  ));
}

다른 방법은 required modifier를 추가하는 것이다. 이는 유저에게 인수를 반드시 입력받아야 하는 경우 유용하다. 이를 적용하면 required가 지정된 파라미터에 인수가 전달되지 않으면 컴파일하지 않는다.

String sayHello({
  required String name, 
  required int age, 
  required String country
}) {
  return "Hello $name, you are $age, and you com from $country";
}

void main() {
  print(sayHello(
    age: 21,
    country: '대한민국',
    name: '영석'
  ));
}

 

Optional Positional Parameters

기존의 Positional Paremeters는 모두 required이다. 만약 일부 파라미터는 required하지 않게 하고 싶다면 Optional Positional Parameters를 활용할 수 있다. required하지 않게하고 싶은 파라미터에 대괄호를 씌워주고 자료형 뒤에다 ?를 붙여준 다음 default value를 할당해주면 된다. ?는 해당 파라미터가 null이 될 수 있다고 표기하는 것이다.

String sayHello(String name, int age, [String? country='대한민국']) => "Hello $name, you are $age, and you com from $country";

void main() {
  print(sayHello('영석', 21));
}

 

이 부분에서 파라미터에 default value 를 할당해주는데 왜 ?를 붙이는지 의문이 들었다. 이와 같은 의문을 가진 사람이 댓글을 달아주셨고 감사하게도 답변이 있어 첨부한다.

질문
[String county = "cuba"]로 지정해주면
country가 null이 될 수 없기 때문에
String? 이 아닌 String을 써도 될 것 같습니다 선생님

답변
금방 아시게 되겠지만, country는 사용자 입력을 받거나 웹API 이용시, 모종의 이유로 null 이 될 가능성이 있습니다. 컴파일시에는 오류가 나지 않지만, 런타임에 오류가 발생하게 됩니다.

간단히 String? 으로 해 두신 채로 입력값에 null을 넣어보면 오류가 나지 않고 출력이 되지만, ?를 빼고 contry에 null을 넣어보면 오류가 나게 됩니다.

 

 

QQ Operator

QQ Operator의 QQ는 Question Question의 약자로 ??나 ?=로 표기되는 연산자이다. null aware operator라고도 불리운다.

 

우선 문자열을 대문자로 바꿔는 함수를 예시로 들어보자. toUpperCase는 String의 메소드이므로 null이면 안된다. 따라서 첫 번째 함수처럼 조건문으로 null일 경우 다른 문자열을 반환해주도록 할 수 있다.

이를 두 번째 함수같이 fat arrow syntax와 ternary operator(삼항 연산자)로 더 간소화 할 수 있다. ternary operator의 문법은 "조건문 ? 참일 경우 : 거짓일 경우"로 작성할 수 있다.

이를 세 번째 함수같이 QQ operator로 더 간소화 할 수 있다. qq는 "좌항 ?? 우항"이 있을 때 좌항이 null이면 우항을 반환하고 아니라면 좌항을 반환한다.

String capitalizeName(String? name) {
  if (name != null) {
    return name.toUpperCase();
  }
  return 'ANON';
}

String capitalizeName(String? name) => name != null ? name.toUpperCase() : 'ANON';

String capitalizeName(String? name) => name?.toUpperCase() ?? 'ANON';

void main() {
  capitalizeName('youngseok');
}

QQ equals 혹은 QQ Assignment Operator라고 불리는 것도 있는데, "좌항 ??= 우항"으로 표시되며 단순히 nullable 변수인 좌항에 null이 오면 우항에 있는 값을 할당해주는 연산자이다. 다음 예시를 보면 이해가 쉽다.

void main() {
  String? name;
  name ??= '영석';
  name ??= '민석';
}

 

Typedef

Typedef는 자료형에 간단한 alias를 붙여주는 것이다. 이는 코드가 길어질 때 가독성 측면에서 유용하다. 다음은 List<int>를 ListOfInts로 alias를 붙여주는 예시이다.

List<int> reverseListOfNumbers(List<int> list) {
  var reversed = list.reversed;
  return reversed.toList();
}

typedef ListOfInts = List<int>;

ListOfInts reverseListOfNumbers(ListOfInts list) {
  var reversed = list.reversed;
  return reversed.toList();
}
typedef UserInfo = Map<String, String>;

String sayHi(UserInfo userInfo) {
  return "Hi ${userInfo['name']}";
}

만약 구조화된 데이터 형태를 지정하고 싶다면 class를 이용하면 된다.

 

CLASSES

class에서 property를 선언할때는 타입을 명시하여 정의한다(function에선 var). Dart에서 인스턴스를 생성할 때는 Java와 같이 new를 사용할 수는 있지만 꼭 그럴 필요는 없다. 또한 클래스 내부의 변수를 호출할 때 this를 사용해도 되긴 하지만 변수와 property의 이름이 겹치는게 아니라면 쓰지 않기를 권고한다.

 

다음 코드는 class에서 property와 method를 선언/활용하는 방법이다.

class Player {
  String name = "영석";
  int xp = 1500;
  
  void sayHello() {
  	// print("Hi my name is ${this.name}")
    print("Hi my name is $name");
  }
}

void main() {
  var player = Player();
  player.name = '민석';
  print(player.name);
  player.sayHello();
}

>>

민석
Hi my name is 민석

 

Constructors

constructor는 인자로 propery를 전달해서 새로운 인스턴스를 생성할 수 있도록 한다. constructor의 이름은 class의 이름과 같아야 한다. Dart는 변수에 값이 비어있는 것을 싫어하기 때문에 property 앞에 late를 붙여주어 나중에 입력받을 것이라는 것을 명시해주어야 한다.

class Player {
  late final String name;
  late int xp;
  
  Player(String name, int xp) {
    this.name = name;
    this.xp = xp;
  }
  
  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var player = Player("영석", 1500);
  player.sayHello();
  var player2 = Player("민석", 2500);
  player2.sayHello();
}

>>

Hi my name is 영석
Hi my name is 민석

 

여기서 생성자의 작업을 더 간략화 할 수 있다. 변수의 타입은 위에서 적어주었기 때문에 각 property의 자료형은 이미 알고있다. 앞선 코드의 경우 name은 String이고, xp는 int임을 알고있다. 그렇기 때문에 별도로 자료형을 다시 명시하지 않고, late를 지우고 인자가 들어올 순서에 맞게 파라미터에 입력받을 변수를 넣으면 된다. 이 경우에는 파라미터의 순서가 중요한 것을 명심해야 한다.

class Player {
  final String name;
  int xp;
  
  Player(this.name, this.xp);
  
  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var player = Player("영석", 1500);
  player.sayHello();
  var player2 = Player("민석", 2500);
  player2.sayHello();
}

 

constructor도 함수처럼 named constuctor parameter를 활용할 수 있다. 활용 방법은 함수와 같다. 단 property가 null이 될 수도 있기 때문에 required를 붙여주거나 default value를 지정해주어야 하는데, 니콜라스는 default value를 선호하지 않는다고 한다.

class Player {
  final String name;
  int xp;
  String team;
  int age;
  
  Player({
    required this.name,
    required this.xp,
    required this.team,
    required this.age,
  });
  
  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var player = Player(
    name: "영석",
    xp: 1500,
    team: "blue",
    age: 21,
  );
  
  var player2 = Player(
    name: "민석",
    xp: 2500,
    team: "red",
    age: 21,
  );
}

 

Named Constructors

기본 constructor랑은 조금 다른 동작의 constructor를 정의할 때는 named constructor를 이용한다. 이는 클래스를 초기화하는 method이다. Dart만의 문법인 콜론을 통해서 클래스를 초기화한다.

class Player {
  final String name;
  int xp;
  String team;
  int age;

  Player({
    required this.name,
    required this.xp,
    required this.team,
    required this.age,
  });

  Player.createBluePlayer({
    required String name,
    required int age,
  })  : this.age = age,
        this.name = name,
        this.team = 'blue',
        this.xp = 0;
  
  Player.createRedPlayer({
    required String name,
    required int age,
  })  : this.age = age,
        this.name = name,
        this.team = 'red',
        this.xp = 0;

  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var bluePlayer = Player.createBluePlayer(
    name: "영석",
    age: 21,
  );
  var redPlayer = Player.createRedPlayer(
    name: "민석",
    age: 21,
  );
}

 

이를 응용하여 API로부터 json을 받아오는 상황에 대해 알아보자. 다음 코드를 살펴보면 Map<String, dynamic>의 비구조화된 데이터로부터 property를 초기화한다.

class Player {
  final String name;
  int xp;
  String team;

  Player.fromJson(Map<String, dynamic> playerJson)
      : name = playerJson['name'],
        xp = playerJson['xp'],
        team = playerJson['team'];

  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var apiData = [
    {
      "name": "영석",
      "team": "red",
      "xp": 0,
    },
    {
      "name": "민석",
      "team": "red",
      "xp": 0,
    },
    {
      "name": "우진",
      "team": "red",
      "xp": 0,
    },
  ];

  apiData.forEach((playerJson) {
    var player = Player.fromJson(playerJson);
    player.sayHello();
  });
}

>>

Hi my name is 영석
Hi my name is 민석
Hi my name is 우진

 

Cascade Operator

Dart에는 cascade operator이란 syntax sugar가 있어 객체의 property를 수정할 때 코드를 더욱 간소화 할 수 있다. "..name"의 첫 번째 .은 minseok을 가리킨다.

class Player {
  String name;
  int xp;
  String team;

  Player({
    required this.name,
    required this.xp,
    required this.team,
  });

  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var youngseok = Player(name: '영석', xp: 1200, team: 'red');
    
  var minseok = youngseok
    ..name = '민석'
    ..xp = 1200000
    ..team = 'blue'
    ..sayHello();
}

 

Enums

Enums는 인자로 넣을 값을 제한해 개발자의 실수를 방지한다. 예를들어 team의 'blue'를 'bule'와 같이 쓰는 경우 말이다. enum 속 요소는 따옴표로 감싸질 필요가 없다.

enum Team { red, blue }
enum XPLevel { beginner, medium, pro }

class Player {
  String name;
  XPLevel xp; // int xp;
  Team team; // String team;

  Player({
    required this.name,
    required this.xp,
    required this.team,
  });

  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var youngseok = Player(name: '영석', xp: XPLevel.medium, team: Team.red);
    
  var minseok = youngseok
    ..name = '민석'
    ..xp = XPLevel.pro
    ..team = Team.blue
    ..sayHello();
}

 

Abstract Classes

Abstract Class는 다른 클래스들이 직접 구현해야하는 메소드들의 일종의 청사진이다. abstract class는 메소드의 이름과 반환 타입만 정하고 구체적인 실행 내용은 작성하지 않는다. abstract class를 상속받는 클래스가 abstract class에 정의되어 있는 메소드를 다시 정의하지 않으면 에러를 반환하여 메소드를 구현하도록 강제한다. 즉 abstract class를 사용하는 클래스는 그 메소드를 갖고있다고 보증하는 것이다. 

abstract class Human {
  void walk();
}

enum Team { red, blue }
enum XPLevel { beginner, medium, pro }

class Player extends Human{
  String name;
  XPLevel xp; // int xp;
  Team team; // String team;

  Player({
    required this.name,
    required this.xp,
    required this.team,
  });
  
  void walk() {
    print("I'm walking");
  }

  void sayHello() {
    print("Hi my name is $name");
  }
}

void main() {
  var youngseok = Player(name: '영석', xp: XPLevel.medium, team: Team.red);
    
  var minseok = youngseok
    ..name = '민석'
    ..xp = XPLevel.pro
    ..team = Team.blue
    ..sayHello();
}

 

Inheritance

상속은 자식 클래스가 부모 클래스의 property와 메소드를 가져온다. 자식 클래스에서 부모 클래스에 property를 전달할 때는 constructor에서 super를 통해 값을 전달해준다. super는 자식 클래스에서 부모 클래스와 상호작용할 수 있게 한다. 이는 positional이나 named나 모두 가능하다.

class Human {
  final String name;
  Human(this.name); // Human({required this.name});
  
  void sayHello() {
    print("Hi my name is $name");
  }
}

enum Team { blue, red }

class Player extends Human {
  final Team team;
  
  Player({
    required this.team,
    required String name
  }) : super(name); // super(name: name);
}

void main() {
  var player = Player(team: Team.red, name:'영석');
  print(player.name);
  player.sayHello();
}

 

sugar syntax로 named constructor에 super를 넘겨주어도 된다.

Player({
    required this.team,
    required super.name,
});

 

자식 클래스에서 부모 클래스의 메소드를 override해줄 수도 있다.

class Human {
  final String name;
  Human(this.name); // Human({required this.name});
  
  void sayHello() {
    print("Hi my name is $name");
  }
}

enum Team { blue, red }

class Player extends Human {
  final Team team;
  
  Player({
    required this.team,
    required String name
  }) : super(name); // super(name: name);
  
  @override
  void sayHello() {
    super.sayHello();
    print("and I play for $team");
  }
}

void main() {
  var player = Player(team: Team.red, name:'영석');
  print(player.name);
  player.sayHello();
}

 

Mixins

Mixin은 constructor가 없는 클래스를 의미한다. 이를 사용할 땐 extends 대신 with을 쓰는데, 상속받지 않고 해당 클래스의 property와 메소드를 긁어온다. Mixin은 여러 클래스를 재사용할 수 있다는 점에서 유용하다. 이는 상속에선 부모 클래스의 constructor를 호출한다는 점과는 구별된다.

mixin Strong {
  final double strengthLevel = 1500.99;
}

mixin QuickRunner {
  void runQuick() {
    print("ruuuuuuuuuuun!");
  }
}

mixin Tall {
  final double height = 1.99;
}

enum Team { blue, red }

class Player with Strong, QuickRunner, Tall {
  final Team team;
  
  Player({
    required this.team,
  });
}

class Horse with Strong, QuickRunner {}

class Kid with QuickRunner {}

void main() {
  var player = Player(team: Team.red,);
  player.runQuick(); 
}

 

다음은 댓글에서 Mixin과 Java에서의 Interface와의 차이에 대한 질문에 대한 니콜라스 쌤의 답변이다. 나중에 헷갈릴 수 있을 것 같아 첨부한다.

Mixin과 Interface는 모두 코드의 재사용성과 유연성을 높이기 위한 개념입니다. 하지만, 둘의 차이점은 다음과 같습니다.

Mixin: Mixin은 클래스에 코드를 재사용하기 위해 사용되며, 다중 상속의 일부 단점을 보완합니다. Mixin은 with 키워드를 사용하여 클래스에 적용합니다.

Interface: Interface는 클래스나 객체가 구현해야 하는 메서드와 속성의 목록을 정의합니다. 즉, 인터페이스는 클래스나 객체가 가져야 하는 기능의 규격을 제공합니다. 인터페이스는 implements 키워드를 사용하여 클래스에서 구현합니다.

Mixin과 Interface는 비슷한 개념이지만, Mixin은 클래스에 코드를 적용하는 데 사용되고, Interface는 클래스나 객체가 가져야 하는 기능의 규격을 정의하는 데 사용됩니다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함