이 글은 노마드코더 - Dart 시작하기를 참고하여 작성하였습니다.
작성자 : 조미서
개발 환경 : Mac OS, Android Studio, Visual Studio Code
먼저 기본적인 Player라는 기본적인 class를 만들어보자
class Player { // class에서 property를 선언할 때는 타입을 꼭 명시해서 정의함
String name = 'choms';
int xp = 1500;
}
void main() {
var player = Player(); // 객체 앞에 new를 쓸 순 있지만, 꼭 쓸 필요는 없다!
print(player.name);
player.name = 'horolro';
print(player.name);
print(player.xp);
}
출력:
choms
horolro
1500
만약 Player의 name을 바꾸지 못하게 고정하고 싶으면 어떻게 해야 할까?
-> 바로 타입 앞에 final을 추가하면 된다.
위 코드에서 final String name = 'choms'라고 선언해 주게 되면 아래의 player.name = 'horolro'는 오류가 생기게 된다.
-> final variable, property는 수정이 불가하기 때문이다.
player가 인사를 할 수 있는 method를 구현해 보기
class Player {
final String name = 'choms';
int xp = 1500;
void sayHello(){
print("Hi my name is $name"); // dart의 클래스에서는 this를 사용할 필요가 없다
// 사용은 가능하지만 같은 이름의 variable가 있어서 어쩔 수 없이 사용하는 게 아닌 이상 사용 필요 X
}
}
void main() {
var player = Player();
player.sayHello();
}
class를 argument(실제로 함수가 호출될 때, 넘기는 변수값)로 player의 이름과 xp를 전달해서 새로운 Player를 생성할 수 있도록 하기 위해 -> constructor method를 작성
class Player{
late final String name; // final property에 값이 없으면 오류가 생기므로 late를 작성
late int xp;
Player(String name, int xp){ // constructor method(생성자 함수) 만들기
//constructor method의 이름은 class와 같아야 함
this.name = name;
this.xp = xp;
}
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var player = Player("choms", 1500);
player.sayHello();
var player2 = Player("dart", 2500);
player2.sayHello();
}
출력:
Hi my name is choms
Hi my name is dart
하지만 위 코드에서 좀 더 단순하게 constuctor를 변형시킬 수 있다.
class Player{
final String name;
int xp;
Player(this.name, this.xp); // 훨씬 더 간결한 형태의 constructor가 생성되었다.
// 주의할 점 : argument의 위치가 중요하다 여기서 (첫 번째는 name, 두 번째는 xp)인 것과 같이
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var player = Player("choms", 1500);
player.sayHello();
var player2 = Player("dart", 2500);
player2.sayHello();
}
만약 class에 더 많은 멤버 변수가 있을 때, argument의 위치를 정확히 지켜서 작성해야 하므로 혼란스러운 경우가 있을 수 있다 -> named argument를 가지는 constructor로 바꾸자!!
class Player{
final String name;
int xp;
String team;
int age;
Player(
{required this.name, // named constructor parameters
required this.xp,
required this.team,
required this.age,}); // {}로 감싸기!
// constructor의 매개변수가 null 일 수 있으니 default 값을 정해주거나 required 키워드를 붙여준다
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var player = Player(
name: "choms",
xp: 1200,
team: 'red',
age: 21,
);
var player2 = Player("dart", 2500, 'blue', 15);
}
이제 데이터가 많은 클래스들을 만들 때도 argument들의 위치를 기억하지 않아도 된다!
(각각을 key와 value 쌍으로 작성하면 됨)
class Player{
final String name;
int xp;
String team;
int age;
Player(
{required this.name, // named constructor parameters
required this.xp,
required this.team,
required this.age,
});
Player.createBluePlayer({ // default로 team이 blue이고 xp는 0인 Player를 생성할 때
required String name, // named syntax 사용 (named parameter는 기본적으로 required 속성이 없음
required int age,
}) : this.age = age, // : 후에 Player 클래스를 초기화
this.name = name,
this.team = 'blue',
this.xp = 0;
Player.createRedPlayer( // default로 team이 red이고 xp는 0인 Player를 생성할 때
String name, // positional syntax 사용 (positional parameter는 기본적으로 required 속성이 있다
int age,
) : this.age = age, // : 후에 Player 클래스를 초기화
this.name = name,
this.team = 'red',
this.xp = 0;
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var player = Player.createBluePlayer(
name: "choms",
age: 21,
);
var redplayer = Player.createRedPlayer(
"dart",
15,
);
}
주석 설명 읽기!
API로부터 데이터를 받음 (Json format과 같이) -> 그 데이터를 class로 바꿔야 하는 경우가 있다
그로 하여 API로부터 여러 Player가 들어있는 목록을 받는다고 가정해 보자.
class Player{
final String name;
int xp;
String team;
Player.fromJson(Map<String, dynamic> playerJson) : // 구조화되지 않은 데이터를 map을 사용해 가져오기
name = playerJson['name'], // Json 속으로 들어가 각각을 초기화
xp = playerJson['xp'],
team = playerJson['team'];
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var apiData = [
{
"name": "choms",
"team": "red",
"xp" : 0,
},
{
"name": "lynn",
"team": "red",
"xp" : 0,
},
{
"name": "dart",
"team": "red",
"xp" : 0,
},
];
apiData.forEach((playerJson) {
var player = Player.fromJson(playerJson);
player.sayHello();
});
}
출력:
Hi my name is choms
Hi my name is lynn
Hi my name is dart
class Player{
String name;
int xp;
String team;
Player({required this.name, required this.xp, required this.team}); // named parameters
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var choms = Player(name: 'choms', xp: 1200, team: 'red') // 먼저 ;을 지운다
..name = 'dart' // choms.부분을 모두 ..으로 변환
..xp = 1500 // ..부분은 choms를 가리켜준다
..team = 'blue'
..sayHello(); // ;은 마지막에만 붙여준다
}
이는 훨씬 코드를 간결하게 작성하고 가독성 좋은 operator이다.
Enum은 우리가 잘못한 실수들을 만들지 않게끔 도와주는 역할을 한다.
(flutter에는 color, margin, padding, style, linement 등과 같은 다양한 옵션이 존재하여 오타가 생길 수 있다.)
enum Team = { red, blue } // 꼭 text 형태로 쓰지 않아도 괜찮다
enum XPLevel {beginner, medium, pro}
class Player{
String name;
XPLevel xp; // enum 사용
Team team; // enum 사용
Player({required this.name, required this.xp, required this.team}); // named parameters
void sayHello(){
print("Hi my name is $name");
}
}
void main() {
var choms = Player(name: 'choms', xp: XPLevel.medium, team: Team.red); // enum 사용
..name = 'dart'
..xp = XPLevel.pro // enum 사용
..team = Team.blue // enum 사용
..sayHello();
}
추상화 클래스로는 객체를 생성할 수 없다
추상화 클래스는 다른 클래스들이 직접 구현해야 하는 메소드들을 모아 놓은 전체적인 청사진 같은 역할
abstract class Human {
void walk(); // 메소드의 이름과 반환 타입만 정해서 정의, parameter 설정 가능, 다른 클래스에서 (상속, 확장) 할 수 있다.
// 추상화 클래스는 특정 메소드를 구현하도록 강제한다
}
enum Team = { red, blue }
enum XPLevel {beginner, medium, pro}
class Player extends Human{
String name;
XPLevel xp;
Team team;
Player({required this.name, required this.xp, required this.team});
void walk(){ // walk 메소드
print('im walking');
}
void sayHello(){
print("Hi my name is $name");
}
}
class Coach extends Human {
void walk(){ // walk 메소드
print('the coach is walking');
}
}
void main() {
var choms = Player(name: 'choms', xp: XPLevel.medium, team: Team.red);
..name = 'dart'
..xp = XPLevel.pro
..team = Team.blue
..sayHello();
}
Human 추상화 클래스가 이를 상속받는 모든 클래스가 가지고 있어야 하는 메소드 (walk)를 정의하고 있다.
정리) 이와 같이 추상화 클래스는 특정 메소드를 구현하도록 강제하지만 그 메소드의 세부적인 명령 사항은 다를 수 있다.
class Human {
final String name;
Human(this.name);
void sayHello() {
print('Hi my name is $name');
}
}
enum Team {blue, red}
class Player extends Human {
final Team team; // final로 변경 못하게
Player({
required this.team,
required String name
}) : super(name); // super라는 키워드르 통해 (확장 한) 부모 클래스와 상호작용을 가능하게 해줌
// Human에서 온 sayHello를 우리가 직접 만든 메소드로 override (대체) 하겠다
void sayHello(){
super.sayHello(); // 확장 (상속) 한 부모 클래스의 프로퍼티에 접근하거나 메소드를 호출할 수 있게 해줌
print('and I play for ${team}');
}
}
void main() {
var player = player(team: Team.red,
name: 'choms',
);
}
확장한 부모 클래스가 생성자를 포함하고 있는데 그 클래스를 다른 어떤 곳에 사용하려면 필요한 값을 전달해야 하고 그 부모 클래스의 생성자를 호출해 줘야만 한다.
코드 분석
main 함수 내 player에서 name(choms)를 받고 Human을 상속받은 Player 클래스 안 생성자의 매개변수 안(required String name)에 name(choms)를 넣은 다음 super, 즉 Human 클래스의 생성자에 즉시 전달해 준다.
Player 클래스 생성자 내의 데이터를 받아서 team은 그대로 놔두고 name은 super 클래스에 전달을 해주어 호출함.
Mixin은 생성자가 없는 클래스를 의미한다.
Mixin 예
class Strong { // 생성자 없음
final double strengthLevel = 1500.99;
}
class QuickRunner { // 생성자 없음
void runQuick() {
print('ruuuuuuuun!');
}
}
class Tall {
final double height = 1.99;
}
class Player with Strong, QuickRunner, Tall { // 다른 클래스의 프로퍼티와 메소드를 모두 긁어옴
}
class Horse with Strong, QuickRunner {} // 다른 클래스에 여러 번 재사용 가능
class Kid with QuickRunner {} // 다른 클래스에 여러 번 재사용 가능
void main() {
player.runQuick(); // 클래스 내부의 메소드 사용도 가능
}
Mixin과 상속(extend)의 차이
extend를 하게 되면 확장한 클래스는 부모 클래스가 되고, 자식 클래스는 부모 클래스를 super를 통하여 접근할 수 있다 그 순간 부모 클래스의 인스턴스가 됨
Mixin은 with라는 키워드를 사용해서 단순히 mixin 내부의 프로퍼티와 메소드들을 가져오는 것뿐인 역할을 한다
Mixin의 조건은 생성자가 없는 클래스여야 한다는 것을 기억하기!