Dart 객체지향

잠만보·2024년 7월 18일

클래스

클래스 선언과 this

C++과 동일하게 class 키워드를 이용해서 클래스를 선언할 수 있음.

// class 키워드를 입력 후 클래스명을 지정해 클래스를 선언합니다.
class Pokemon {
  // ❶ 클래스에 종속되는 변수를 지정할 수 있습니다.
  String name = '잠만보';

  // ❷ 클래스에 종속되는 함수를 지정할 수 있습니다.
  // 클래스에 종속되는 함수를 메서드라고 부릅니다.
  void sayName() {

    // ❸ 클래스 내부의 속성을 지칭하고 싶을 때는 this 키워드를 사용하면 됩니다.
    // 결과적으로 this.name은 Idol 클래스의 name 변수를 지칭합니다.
    print('저는 ${this.name}입니다.');
    // ➍ 스코프 안에 같은 속성 이름이 하나만 존재한다면 this를 생략할 수 있습니다.
    print('저는 $name입니다.');
  }
}

void main() {

  Pokemon poke1 = Pokemon();  // ➊ Pokemon 인스턴스 생성

  // 메서드를 실행합니다.
  poke1.sayName();
}

this 포인터
클래스에 종속되어있는 값을 지칭할 때 사용.
함수 내부에 같은 이름의 변수가 없으면 this 생략 가능

생성자

클래스의 인스턴스(실체)를 생성하는 메서드.

class Pokemon {
  // ❶ 생성자에서 입력받는 변수들은 일반적으로 **final 키워드 사용**
  final String name;

  // ❷ 생성자 선언
  // 클래스와 같은 이름이어야 합니다.
  // 함수의 매개변수를 선언하는 것처럼 매개변수를 지정해줍니다.
  Pokemon(String name) : this.name = name;

  void sayName() {
    print('저는 ${this.name}입니다.');
  }
}

void main() {
  // name에 '잠만보' 저장
  Pokemon poke1 = Pokemon('잠만보');
  poke1.sayName();

  // name에 '피카츄' 저장
  Pokemon poke2 = Pokemon('피카츄');
  poke2.sayName();
}

//실행결과
저는 잠만보입니다.
저는 피카츄입니다.

생성자 함수의 역할은 객체의 멤버변수의 값을 초기화 하는 것이다.
현재 Pokemon 클래스에 멤버변수는 name 하나 이므로
Pokemon(String name) : this.name = name;
으로 name 변수를 파라미터 String name 으로 초기화해 준다.

위에 Pokemon(String name) : this.name = name; 코드를
Pokemon(this.name); 으로 간략화 할 수도 있다.

네임드 생성자

{클래스명.네임드 생성자명} 형식

class 클래스명{
  클래스명.생성자명(){
  }
}

말 그대로 생성자에 이름을 부여한 것이다.
하나의 클래스 내에 많은 생성자를 생성하거나 생성자를 명확히 하기 위해서 사용한다.

class PokemonTrainer {
  final String name;
  final int membersCount;

  // ❶ 생성자
  PokemonTrainer(String name, int membersCount)
  // 1개 이상의 변수를 저장하고 싶을 때는 , 기호로 연결해주면 됩니다.
      : this.name = name,
        this.membersCount = membersCount;

  // ❷ 네임드 생성자
  // {클래스명.네임드 생성자명} 형식
  // 나머지 과정은 기본 생성자와 같습니다.
  PokemonTrainer.fromMap(Map<String, dynamic> map)
      : this.name = map['name'],
        this.membersCount = map['membersCount'];

  void sayName() {
    print('저희는 ${this.name}입니다. ${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

void main() {
  // 기본 생성자 사용
  PokemonTrainer party1 = PokemonTrainer('사천왕', 4);
  party1.sayName();

  // fromMap이라는 네임드 생성자 사용
  PokemonTrainer party2 = PokemonTrainer.fromMap({
    'name': '챔피언',
    'membersCount': 7,
  });
  party2.sayName();
}

// 실행결과
저희는 사천왕입니다. 사천왕 멤버는 4명입니다.
저희는 챔피언입니다. 챔피언 멤버는 7명입니다.

참고
이름없는 생성자는 하나만 선언 가능
네임드 생성자를 선언하면 기본 생성자 생략 불가능

Private 변수

일반적인 언어에서의 Private 변수는 클래스 내부에서만 사용가능한 변수를 뜻하지만
Dart에서의 Private 변수는 같은 파일에서만 접근 가능한 변수를 뜻한다.

_ 을 변수명 앞에 붙여서 Private 변수 선언을 한다.

class Pokemon {
  // ❶ '_'로 변수명을 시작하면
  // 프라이빗 변수를 선언할 수 있습니다.
  String _name;

  Pokemon(this._name);
}

void main() {
  Pokemon poke1 = Pokemon('잠만보');

  // 같은 파일에서는 _name 변수에 접근할 수 있지만
  // 다른 파일에서는 _name 변수에 접근할 수 없습니다.
  print(poke1._name);
}

Getter / Setter

Getter는 값을 가져올 때 사용.
Setter는 값을 지정할 때 사용.

최근 객체지향 프로그래밍을 할 때 변수의 값에 불변성(인스턴스화 후 변경할 수 없는 특성)을 특성으로 사용하기 때문에 Setter는 거의 사용하지 않는다.

class Pokemon {
  String _name= '잠만보' ;

  // ❶ get 키워드를 사용해서 게터임을 명시합니다.
  // 게터는 메서드와 다르게 매개변수를 전혀 받지 않는다.
  String get getName {
    return this._name;
  }

  // ❷ 세터는 set이라는 키워드를 사용해서 선언합니다.
  // 세터는 매개변수로 딱 하나의 변수를 받을 수 있습니다.
  set setName(String name) {
    this._name = name;
  }
}

void main() {
  Pokemon poke1 = Pokemon();

  poke1.setName = '메타몽'; // ❶ 세터로 name을 '메타몽' 으로 변경
  print(poke1.getName);      // ❷ 게터로 변경된 name 값 가져와서 출력
}

상속

어떤 클래스의 기능을 다른 클래스가 사용할 수 있게 하는 것.
extend 키워드를 사용해서 상속 가능

형식
class 자식 클래스 extends 부모 클래스{

  // 자식클래스에서 새롭게 멤버변수 생성 가능, 대신에 생성자 함수에서 초기화 잘 해줘야 함
  final int newVal;

  // 생성자 함수
  자식 클래스(부모클래스 멤버변수들 + 새롭게 생성한 멤버변수{this.newVal}): super( 부모클래스 멤버변수들)
}
class PokemonTrainer {
  final String name;
  final int membersCount;

  PokemonTrainer(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

// ❶ extends 키워드를 사용해서 상속받습니다.
// class 자식 클래스 extends 부모 클래스 순서입니다.
class PokemonMaster extends PokemonTrainer {
  final int age; // 자식 클래스에서 새로운 멤버변수 선언

  // ❷ 상속받은 생성자
  PokemonMaster(
      String name,
      int membersCount,
      {this.age = 10} // 생성자에서 age 변수 초기화, default 값 0
      ) :  super(   // super는 부모 클래스를 지칭합니다. 부모 클래스에서 name, membersCount 상속받았다.
    name,
    membersCount,
  );

  // ❸ 상속받지 않은 기능
  void sayMale() {
    print('저는 남자 포켓몬 마스터 입니다.');
  }

  void sayAge() {
    print('저는 ${age}살 입니다.');
  }
}

void main() {

  PokemonMaster group1 = PokemonMaster('로켓단', 7);  // 생성자로 객체 생성
  PokemonMaster group2 = PokemonMaster('플라즈마단', 10, age: 20); // default 값 대신에 전달하려면 age : 이렇게 해야함

  group1.sayName();          // ❶ 부모한테 물려받은 메서드
  group1.sayMembersCount();  // ❷ 부모한테 물려받은 메서드
  group1.sayMale();          // ❸ 자식이 새로 추가한 메서드
  group1.sayAge();
  print("");
  group2.sayName();          // ❶ 부모한테 물려받은 메서드
  group2.sayMembersCount();  // ❷ 부모한테 물려받은 메서드
  group2.sayMale();          // ❸ 자식이 새로 추가한 메서드
  group2.sayAge();

}

// 실행결과
저는 로켓단입니다.
로켓단 멤버는 7명입니다.
저는 남자 포켓몬 마스터 입니다.
저는 10살 입니다.

저는 플라즈마단입니다.
플라즈마단 멤버는 10명입니다.
저는 남자 포켓몬 마스터 입니다.
저는 20살 입니다.

오버라이드

오버라이드는 부모 클래스 또는 인터페이스에 정의된 메소드를 재정의할 때 사용된다.(함수 덮어쓰기)

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

class GirlGroup extends Idol {
  // 2.3 상속에서처럼 super 키워드를 사용해도 되고 다음처럼 생성자의 매개변수로 직접 super 키워드를 사용해도 됩니다.
  GirlGroup(
      super.name,
      super.membersCount,
      );

  // ❶ override 키워드를 사용해 오버라이드 합니다. Idol 클래스의 sayName 함수 재정의
  @override
  void sayName() {
    print('저는 여자 아이돌 ${this.name}입니다.');
  }
}

void main() {
  GirlGroup redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName(); // ❶ 자식 클래스의 오버라이드된 메서드 사용

  // sayMembersCount는 오버라이드하지 않았기 때문에
  // 그대로 Idol 클래스의 메서드가 실행됩니다.
  redVelvet.sayMembersCount(); // ❷ 부모 클래스의 메서드 사용
}

// 실행결과
저는 여자 아이돌 블랙핑크입니다.
블랙핑크 멤버는 4명입니다.

인터페이스

상속은 공유되는 기능을 이어받는 개념이지만
인터페이스는 공통으로 필요한 기능을 정의만 해두는 역할을 말한다.

implements 키워드를 사용하면 원하는 클래스를 인터페이스로 사용할 수 있다.

상속은 단 하나의 클래스만 할 수 있지만, 인터페이스는 적용 개수에 제한이 없다.
여러 인터페이스를 사용하고 싶으면 , 기호를 사용하여 인터페이스를 나열해 입력해주면 된다.

상속 vs 인터페이스

상속에서는 부모 클래스의 모든 기능이 상속되므로 재정의할 필요가 없다

반면에 인터페이스는 반드시 모든 기능을 다시 재정의 해주어야 한다.

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

// ❶ implements 키워드를 사용하면 원하는 클래스를 인터페이스로 사용할 수 있습니다.
class GirlGroup implements Idol {
  final String name;
  final int membersCount;

  GirlGroup(
      this.name,
      this.membersCount,
      );
      
// 인터페이스 이므로 반드시 재정의 해 주어야 함
  void sayName() {
    print('저는 여자 아이돌 ${this.name}입니다.');
  }
  
// 인터페이스 이므로 반드시 재정의 해 주어야 함
  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

void main() {
  GirlGroup blackPink = GirlGroup('블랙핑크', 4);

  blackPink.sayName();
  blackPink.sayMembersCount();
}

믹스 인

특정 클래스에 원하는 기능들만 골라 넣을 수 있는 기능이다.

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

mixin IdolSingMixin on Idol{
  void sing(){
    print('${this.name}이 노래를 부릅니다.');
  }
}

// 믹스인을 적용할 때는 with 키워드 사용
class BoyGroup extends Idol with IdolSingMixin{
  BoyGroup(
      super.name,
      super.membersCount,
      );

  void sayMale() {
    print('저는 남자 아이돌입니다.');
  }
}

void main(){
  BoyGroup bts = BoyGroup('BTS', 7);

  // 믹스인에 정의된 sing() 함수 사용 가능. Idol 클래스에는 없는 sing() 함수 사용
  bts.sing();
}

추상

상속이나 인터페이스로 사용하는 데 필요한 속성만 정의하고 인스턴스화 할 수 없도록 하는 기능이다.

즉, 추상 클래스로 필요한 틀만 짜 놓고 상속이나 인터페이스로 구현해서 인스턴스화 하는 것이다.

추상 메서드는 함수의 반환 타입, 이름, 매개변수만 정의하고 하수 바디의 선언을 자식 클래스에서 필수로 정의하도록 강제한다.

추상 메서드는 부모 클래스를 인스턴스화 할 일 이 없고, 자식 클래스에게 필수적 또는 공통적으로 정의돼야 하는 메서드가 존재할 때 사용된다.

// ❶ abstract 키워드를 사용해 추상 클래스 지정
abstract class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount); // ❷ 생성자 선언

  void sayName();          // ❸ 추상 메서드 선언
  void sayMembersCount();  // ➍ 추상 메서드 선언
}

// implements 키워드를 사용해 추상 클래스를 **구현하는** 클래스
class GirlGroup implements Idol {
  final String name;
  final int membersCount;

  GirlGroup(
      this.name,
      this.membersCount,
      );

  void sayName() { // sayName 구현
    print('저는 여자 아이돌 ${this.name}입니다.');
  }

  void sayMembersCount() { // sayMembersCount 구현
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

void main() {
  GirlGroup redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName();
  redVelvet.sayMembersCount();
}

제네릭

C++의 템플릿과 제네릭 생각하면 된다

// 인스턴스화할 때 입력받을 타입을 T로 지정합니다.
class Cache<T> {
  // data의 타입을 추후 입력될 T 타입으로 지정합니다.
  final T data;

  Cache({
    required this.data,
  });
}

void main() {
  // T의 타입을 List<int>로 입력합니다.
  final cache = Cache<List<int>>(
    data: [1,2,3],
  );

  // 제네릭에 입력된 값을통해 data 변수의 타입이 자동으로 유추됩니다.
  // reduce() 함수가 기억나지 않는다면 1.3.1절 List타입의 reduce() 함수를 복습해보세요.
  print(cache.data.reduce((value, element) => value + element));
}
// 실행결과 
6

스태틱(Static)

Static 키워드를 사용하면 클래스 자체에 귀속된다
예시를 보면서 이해해보자

class Counter{
  // ❶ static 키워드를 사용해서 static 변수 선언
  static int i= 0;


  // ❷ static 키워드를 사용해서 static 변수 선언
  Counter(){
    i++;
    print(i);
  }
}

void main() {
  Counter count1 = Counter();
  Counter count2 = Counter();
  Counter count3 = Counter();
}

// 실행결과
1
2
3

변수 int iStatic으로 지정했기 때문에 인스턴스를 호출할 때 마다 생성자 함수가 호출되고, i++ 이 실행되서 i 가 1씩 증가한다.
또한 생성자 함수를 보면 this.i 가 아니라 i로 명시했는데 이는 static 변수가 클래스에 직접 귀속되기 때문에 생성자에서 static 값을 지정하지 못하기 때문이다.

따라서 static 키워드는 인스턴스끼리 공유해야 하는 정보가 있을 때, 사용하면 된다.

캐스케이드 연산자

인스턴스에서 해당 인스턴스의 속성이나 멤버 함수를 연속해서 사용하는 기능이다.

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

void main() {
  // cascade operator (..)을 사용하면
  // 선언한 변수의 메서드를 연속으로 실행할 수 있습니다.
  Idol blackpink= Idol('블랙핑크', 4)
    ..sayName()
    ..sayMembersCount();
}

즉, 따로 blackpink.sayName(); 이렇게 할 필요 없이 ..기호를 사용해서 더 간결하게 코드를 작성할 수 있다.

profile
아프지 말자 - (잘못된 정보, 수정 사항 있으면 언제든지 알려주시면 감사하겠습니다!)

0개의 댓글