欲速不達

일을 급히 하고자 서두르면 도리어 이루지 못한다.

Fantastic AI, Fantastic World

CS | Computer Science/FE | Front-end

[App] Dart 5. Classes

_껀이_ 2024. 4. 19. 21:19
728x90
반응형

Classes

클래스는 OOP의 꽃이다.

모든 것이 클래스로 구현되어 있고, 앞으로 클래스로 코드를 작성해서 사용할 일이 많을 것이다.

이번 포스팅에서는 Dart의 클래스에 대해서 알아보자.


1) Default Class

간단한 클래스를 구성해보자.

 

class Player {
  // property
  String name = 'kuuneeee'; // final을 붙이면 수정이 안됨
  int xp = 1500;

  void sayHello() {
    print("Hi my name is $name");
    // $this.name이라고 해도 작동은 함 -> 하지만 클래스 내에서 권고되지 않음
    // 클래스 내에 겹치는 변수명(같은 이름의 name 변수)가 있으면 this.name이라고 사용할 수는 있음
    // this는 현재 객체(인스턴스, 여기선 Player 클래스)를 가리킴 -> this.name = Player.name
  }
}

void main() {
  var player = Player(); //Player 인스턴스 생성, new Player라고 안해도 됨 -> 작동은 함
  // print(player.name);
  // player.name = 'lalalalala';
  // print(player.name);

  player.sayHello();
}

 

Player 클래스를 선언하고 그 안에 property를 선언한다.

그리고 Player 클래스 안에 sayHello 메서드도 작성한다. 

 

그리고 main 함수 내에서 Player 인스턴스를 생성해주고 sayHello 메서드를 실행한다.

 

이렇게 아주 간단하게 클래스 예제를 만들 수 있다.

 

위 코드의 sayHello 메서드 내에서 String에 $name으로 property에 접근한다.

this는 현재 인스턴스를 의미하며 여기서는 Player 클래스의 인스턴스를 의미한다.

this.name이라고 접근할 수도 있지만, 메서드 내에서 해당 클래스의 property에는 바로 접근할 수 있으므로 권고되지 않는다.

단지 메서드 내의 지역변수 명이 클래스 property와 같은 경우 구분을 위해 써줄 수 있다. 


2) Constructors

Constructor는 생성자라고도 하며, 클래스의 입력을 다룬다.

예시를 보자.

 

2-1) Default Constructors

class Player {
  // constructor에서 값을 받을 거라서 late
  late final String name;
  late int xp;

  // constructor
  // 클래스 이름과 같아야 함
  Player(String name, int xp) {
    this.name = name;
    this.xp = xp;
  }

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

void main() {
  var player = Player('kuuneeee', 1500);
  player.sayHello();

  var player2 = Player('naerong', 2500);
  player2.sayHello();
}

 

생성자는 클래스 안에 위치하고 있으며, 클래스명과 같은 이름으로 선언한다.

위의 코드에서 보면 생성자 Player는 String name과 int xp를 입력으로 받아 this를 사용해서 property에 할당하는 역할을 한다. 이때 property로 선언하는 name과 xp는 생성자에서 값을 할당해주므로, late 변수를 사용한다.

 

2-2) Short Constructors

위의 코드를 짧게 쓰면 다음과 같아진다.

 

class Player {
  // late 사용 x
  final String name;
  int xp;

  // constructor
  // 순서 중요 -> 이런식으로 줄여서 작성 할 수 있음 => positional parameter(argument)
  Player(this.name, this.xp);

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

void main() {
  var player = Player('kuuneeee', 1500);
  player.sayHello();

  var player2 = Player('lynn', 2500);
  player2.sayHello();
}

 

positional parameter의 특성을 사용해서 생성자 Player의 입력 위치에 곧바로 this로 property에 할당한다.

이때 property에는 late을 사용하지 않는다.

 

앞선 방법에서는 입력 받는 과정이 있고 그 name과 xp를 할당해주는 2단계의 과정을 거치지만,

위의 코드에서는 재할당하는 과정을 생략했기 때문에 late를 사용하지 않아도 된다.


3) Named Constructor  : Parameters

이전 포스팅에서 다뤘던 named parameter와 마찬가지로, 생성자에서 사용하는 named constructor parameters가 있다.

예시를 보자.

 

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

  // constructor
  1) positional parameters
  // positional parameters(arguments)는 많아지면 통제하기 어려워질 수 있음
  Player(this.name, this.xp, this.team, this.age);
  
  2) named constructor parameters
  Player({
    required this.name, // null 값이 들어올 수 있기 때문에 required
    required this.xp,
    required this.team,
    required this.age,
  });

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

void main() {
  // named parameter와 같음 -> required 변수는 없으면 안됨
  var player = Player(
    name: 'kuuneeee',
    xp: 1200,
    team: 'red',
    age: 25,
  );
  player.sayHello();

  var player2 = Player(
    name: 'naerong',
    xp: 2500,
    team: 'blue',
    age: 20,
  );
  player2.sayHello();
}

 

이전 포스팅에서 설명한 대로 positional parameters는 수가 많아질수록 헷갈리거나 통제하기 어려울 수 있다.

그러므로 named parameters를 사용하며, 생성자에서도 마찬가지로 사용할 수 있다.

 

생성자에서는 입력되는 인자들을 {}로 감싸주고, null 값 처리를 위해 required라고 명시할 수 있다.

호출 시에는 인자를 key와 value 형태로 입력해준다.


4) Named Constructors : ' : ' (colon)

dart만의 특징인 ' : '의 예시를 보자.

 

class Player {
  final String name;
  int xp, age; // int 변수가 여러개면 한번에 써도 됨
  String team;

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

  // named constructor
  // 플레이어를 초기화하는 method
  Player.createBluePlayer({
    // named parameter(argument)는 기본적으로 변수를 required하지 않기 때문에 써줘야 함
    required String name, // createBluePlayer 메서드에서는 name, age를 required하고
    required int age,
  })  : this.name = name, // : 다음에 오는 걸로 property를 초기화하는 것
        this.age = age, // name, age는 받아온 걸 사용
        this.team = 'blue',
        this.xp = 0;

  Player.createRedPlayer(String name, int age)
      // positional parameter(argument) -> 전부 required
      : this.name = name,
        this.age = age,
        this.team = 'red',
        this.xp = 0;

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

// xp가 0으로 초기화된 red, blue팀 각각의 Constructor를 만들고 싶다면?
void main() {
  var bluePlayer = Player.createBluePlayer(
    name: 'kuuneeee',
    age: 25,
  );
  var redPlayer = Player.createRedPlayer('naerong', 20);
  bluePlayer.sayHello();
  redPlayer.sayHello();
}

 

앞선 생성자에서는 입력된 값으로 player를 생성했었다.

xp 값이나 team 같은 인자들도 직접 입력했었다. 그런데 실제로 새로 계정을 만들어서 로그인 할 때는, xp는 0 team은 랜덤이거나 지정된 팀에 배정이 될 것이다.

 

만약 xp가 0으로 초기화된 red, blue 팀 각각의 생성자를 만들고 싶다면 어떻게 해야할까?

우선, createRedPlayer와 createBluePlayer와 같은 메서드를 만들어준다.

그리고 ' : '을 사용해서 property를 초기화하면 된다.

 

메서드 뒤에 ' : '를 붙이고 그 다음 초기화할 내용을 넣어주면 된다.이때 괄호 등을 사용하지 않고 콤마(,)로만 구분하며 제일 마직막에 세미콜론(;)을 붙여준다.

 

위의 코드를 보면

createBluePlayer는 named parameter를 사용했고, createRedPlayer는 posional parameter를 사용했다. named parameter를 사용할 때는 변수 타입 앞에 required를 붙인다. named parameter는 자동으로 required되지 않기 때문이다. 그리고 둘다 입력으로 받은 name과 age로 property를 초기화하고, xp는 0, team은 각각 blue와 red로 초기화한다.

 

 

# 참고

만약 api를 호출해서 JSON에 있는 데이터를 입력받는 생성자를 만드려면 어떻게 할까?

 

class Player {
  final String name;
  int xp; // int 변수가 여러개면 한번에 써도 됨
  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": "kuuneeee",
      "team": "red",
      "xp": 0,
    },
    {
      "name": "naerong",
      "team": "blue",
      "xp": 0,
    },
    {
      "name": "bbaksso",
      "team": "red",
      "xp": 0,
    },
  ];

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

 

생성자에 fromJSON이라는 메서드를 만들고 JSON 데이터를 입력받게끔 하면 된다.

 

위의 코드에서는 apiData로 JSON과 같이 임의로 데이터를 만들었고, forEach로 하나씩 입력하는 걸로 구현했다.

main에서는 apiData 리스트에 있는 요소 하나씩 fromJSON에 입력되고, 생성자에서 playerJson으로 받아 각각의 키값으로 접근해서 property를 초기화한다.


5) Cascade Notation

cascade notation은 메서드를 출력할때 object의 이름을 생략하기 위한 장치이다.

 

 

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 nico = Player(name: 'kuuneeee', xp: 1200, team: 'red');
  kuuneeee.name = 'las';
  kuuneeee.xp = 120000;
  kuuneeee.team = 'blue';
}

 

이런 클래스가 있다고 하자.

 

main에서 Player의 property에 접근하기 위해 kuuneeee를 반복적으로 호출하게 된다. Dart에서는 이렇게 반복적으로 인스턴스를 호출할 때 dot(.)을 사용해서 생략할 수 있다.

 

void main() {
  1) 
  var kuuneeee = Player(name: 'kuuneeee', xp: 1200, team: 'red')
    ..name = 'las' // 앞 . : 직전에 있는 클래스를 지칭 -> 여기선 kuuneeee
    ..xp = 120000
    ..team = 'blue'
    ..sayHello(); // 메서드도 호출 가능
  
  2) 
  var kuuneeee = Player(name: 'kuuneeee', xp: 1200, team: 'red');
  
  var potato = kuuneeee
    ..name = 'las' // 앞 . : potato = kuuneeee = Player
    ..xp = 120000
    ..team = 'blue'
    ..sayHello(); 
}

 

이처럼 dot(.)은 직전에 있는 클래스의 인스턴스를 지칭하게 된다.

단, cascade operator를 사용하려면 인스턴스 선언 후 ;을 사용하면 안된다.

 

즉, 붙어 있는 object에 쉽게 접근하게 하려는 장치이므로, 접근하려는 인스턴스 등에 붙여 사용하면 된다. 


6) Enums

Enum은 선택의 폭을 좁혀서 실수하지 않게끔 하는 장치이다.

예시를 보자.

 

enum Team { red, blue }
// 'red', 'blue' 이렇게 안써도 된다 -> red, blue는 새로 지정한 Team 타입이 됨

enum XPLevel { beginner, intermediate, advanced }

class Player {
  String name;
  XPLevel xp; // XPLevel 타입으로 지정
  Team team; // Team 타입으로 지정 -> red, blue만 있으니까 team 인자에는 저 둘만 오게 됨

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

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

void main() {
  var kuuneeee = Player(name: 'kuuneeee', xp: XPLevel.beginner, team: Team.red); // 바뀐 enum 타입으로 지정
  var potato = kuuneeee
    ..name = 'las'
    ..xp = XPLevel.intermediate // XPLevel에서 호출
    ..team = Team.blue // Team에서 호출
    ..sayHello();
}

 

Player라는 클래스에는 name, xp, team을 입력으로 한다.

이때, String이나 int와 같은 타입만 맞으면 어떤 것이든 입력이 되기 때문에 실수가 생길 수 있다.

이를 방지하기 위해서 입력할 수 있는 종류를 제한하는 것이 Enum이다.

 

위의 코드처럼 enum 변수를 생성한다. Team이라는 이름으로 { red, blue } 두 종류로 선언한 후, property 선언시 team의 데이터 타입으로 Team으로 지정한다.

그리고 main에서 입력 시에 Team.red 또는 Team.blue로 입력하게끔 하면 입력되는 종류를 제한할 수 있다.


7) Abstract Classes

추상화 클래스는 다른 클래스들이 직접 구현해야 하는 메서드들을 모아 놓은 일종의 blueprint이다.

그래서 object를 생성할 수 없고, 형태만을 가진다.

 

abstract class Human {
  void walk(); // Human 클래스는 walk 메서드를 가지고, void를 반환함
  // 이걸 사용하면 다른 클래스들이 이 형태를 사용할 수 있음
  // 메서드의 이름과 반환 타입, 파라미터만 정해서 정의
}

enum Team { red, blue }

enum XPLevel { beginner, intermediate, advanced }

class Player extends Human {
  // Human 클래스를 상속
  String name;
  XPLevel xp;
  Team team;

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

  void walk() {
    // 추상화 클래스에서 상속한 메서드를 지정해줘야 함
    print('I\'m walking...');
    // Flutter에서는 그다지 많이 사용하지 않을 것
  }

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

class Coach extends Human {
  void walk() {
    print('the coach is walking...');
  }
}
// extends Human이 있다면 해당 클래스는 walk 메서드를 가지고 있다는 것을 알 수 있음
// 하지만 각각의 클래스의 walk 메서드는 다르게 정의될 수 있음

 

Human이라는 추상화 클래스는 void 값을 return하는 walk 메서드를 가진다.

Player 클래스가 Human을 상속하면 (extends Human) 마찬가지로 walk라는 메서드를 가져야한다.

 

즉, 추상화 클래스는 어떤 클래스가 가져야되는 메서드들을 모아놓은 집합체라고 볼 수 있다.

단, 서로 다른 여러 클래스에서 추상화 클래스를 상속하더라도 각각의 클래스 안의 walk 메서드는 다르게 정의될 수 있다.

 

추상화 클래스에 있는 메서드는 상속받은 클래스에 포함되야 하므로, 자식 클래스에 어떤 메서드가 포함되었는지를 알 수 있다. 하지만 flutter에서는 그다지 사용하지 않는 기능이라고 한다.


8) Inheritance

상속은 추상화 클래스 설명에서 봤듯이 어떤 클래스에 있는 메서드나 property를 가져오는 것이다.

 

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

enum Team { red, blue }

class Player extends Human {
  // Player는 Human을 상속
  final Team team;

  Player({
    required this.team,
    required String name,
  }) : super(name: name);
  // super를 통해서 Human 클래스의 name으로 접근
  // Human에서 name이 requied 변수이기 때문에 : 써서 초기화 -> required 변수 아니면 그냥 name

  @override // 덮어씌우기
  void sayHello() {
    super.sayHello(); // super를 통해서 Human 클래스의 sayHello을 호출
    print('and I play for ${team}');
  }
}

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

 

Human이라는 클래스와 Player라는 클래스를 각각 선언한다. Player는 Human을 상속하게 한다.

Human에는 name을 property로 가지고 있으며, Player에는 team을 property로 가지고 있다. 이때 Human을 상속받았으므로 Player는 name도 property로 가지게 된다.

 

단, Player 클래스 내부에는 name이 변수로 있지 않다.

그러므로 super()를 사용해서 Human에 있는 name 변수에 접근하게 된다.

위의 코드에서는 Player는 name과 team을 입력으로 받아 super()를 사용하여 Human에 있는 name을 초기화하게 된다.

 

또, 부모 클래스의 메서드를 override할 수도 있다.

@override라고 작성하고, 덮어씌우려는 메서드를 작성한다. 그리고 super.sayHello()로 메서드에 접근해서 추가로 String을 붙일 수 있다. 


9) Mixins

Mixin은 생성자가 없는 클래스상속과 유사하게 다른 클래스의 property나 메서드를 사용할 수 있는 클래스이다.

 

mixin class Strong {
  // 버전이 바뀌면서 mixin, mixin class로 사용해야 함
  final double strenghtLevel = 1500.99;
}

mixin QuickRunner {
  void runQuick() {
    print('runnnnnnn!');
  }
}

mixin Tall {
  final double height = 1.99;
}

enum Team { red, blue }

class Player with Strong, QuickRunner, Tall {
  // 상속하는게 아니라 다른 클래스들의 property나 메서드를 그냥 긁어오는 것
  // Flutter나 Flutter 플러그인 사용할때 많이 씀
  final Team team;
  String name;

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

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

  String sayHello2() {
    return "Hello my name is $name";
  }
}

// Mixins은 여러 클래스에서 재사용이 됨
class Horse with Strong, QuickRunner {}

class Kid with QuickRunner {}

void main() {
  var player = Player(
    team: Team.red,
    name: 'kuuneeee',
  );
  var kid = Kid();

  player.sayHello();
  print(player.strenghtLevel); // print는 하나의 값만 출력 가능

  print('${player.sayHello2()} and my power is ${player.strenghtLevel}');
  // void 함수는 못함 -> 두 가지를 쓰려면 이렇게 값을 가져와서 print

  print('Kid...');
  kid.runQuick(); // 메서드는 () 없으면 메서드 자체로 호출됨 -> 파이썬과 같음
}

 

mixin은 mixin 또는 mixin class를 사용해서 선언한다. mixin은 생성자가 없이 property나 메서드만 가지게된다.

mixin을 사용할 때는 extends가 아니라 with로 사용하고 mixin을 사용한 클래스는 mixin의 property나 메서드를 그대로 사용할 수 있다.

728x90
반응형

'CS | Computer Science > FE | Front-end' 카테고리의 다른 글

[App] Dart 4. Functions  (0) 2024.04.19
[App] Dart 3. Data Types  (0) 2024.04.18
[App] Dart 2. Variables  (0) 2024.04.18
[App] Dart 1. say "Hello World!"  (0) 2024.04.18