Dart 객체지향 프로그래밍

원종인·2022년 6월 22일
0

Dart 공부

목록 보기
2/4

생성자

생성자를 통해서 클래스 내의 변수에 내가 직접 변수를 넣어줄 수 있다.
외부에서 값을 받아서 클래스 내부의 변수에 넣어줄 수 있다.
this는 현재 위치하는 클래스를 의미한다.

void main() {}

class Idol {
  String name;
  List<String> members;

  Idol(String name, List<String> members)
      : this.name = name,
        this.members = members;
}

이는 더 간결하게 만들어 줄 수 있다.
그렇지만 생성자에 자료형이 안맞아도 미리 선언해둔 변수들이 있기에 거기에 맞추어서 값을 넣어줘야 한다.

class Idol {
  String name;
  List<String> members;

  Idol(this.name, this.members)
}

named constructor - 다른 방식으로도 생성자를 받을 수 있다는 것을 보여주는 표현

void main() {
	Idol bts = Idol.fromList(
    	[
        	["RM","진","슈가","제이홉","지민","뷔","정국"],
            "BTS"
        ]
    )
}

class Idol {
  String name;
  List<String> members;

  Idol(this.name, this.members)
  Idol.fromList(List values)
  	: this.members = values[0],
      this.name = values[1];
}

요즘에는 immutable 프로그램이 대세이다. 즉 이미 선언된 인스턴스는 변하지 않고 만약 바꾸고 싶으면 그냥 새로 만드는 방식을 선호한다.
그래서 대부분의 상황에서는 클래스의 변수에 final을 선언하게 된다.
인스턴스를 만들때 final이 생성되는 것이므로 그 이후로 바꿀수 없게 된다.

class Idol {
  final String name;
  final List<String> members;

  Idol(this.name, this.members);
}

생성자 앞에 const를 붙여서 쓴다. const의 장점은 그냥 const를 써도 되고 안써도 된다.
const를 선언할 수 있는 변수들로 구성된 parameter를 사용했을 때, const로 인스턴스를 만들 수 있다.

void main() {
	Idol bts = const Idol(
    	"BTS",
        ["RM","진","슈가","제이홉","지민","뷔","정국"]
    );
}

class Idol {
  final String name;
  final List<String> members;

  const Idol(this.name, this.members)
}

재미있는 것은 const로 선언해서 같은 값을 넣어준다면 같은 인스턴스가 된다는 것이다. 원래는 안된다. 아래처럼 const를 선언안하고 같은 값으로 인스턴스를 만든다면 서로가 다르다는 false가 나오지만 const로 선언한다면 둘의 값이 모두 같을 경우 서로 같다는 true가 나온다. 즉 const로 선언하게 되면 같은 인스턴스가 된다는 것 잘 알아둘 것

void main() {
	Idol bts1 = Idol(
    	"BTS",
        ["RM","진","슈가","제이홉","지민","뷔","정국"]
    );
    Idol bts2 = Idol(
    	"BTS",
        ["RM","진","슈가","제이홉","지민","뷔","정국"]
    );
    
    print(bts1 == bts2)
}

class Idol {
  final String name;
  final List<String> members;

  const Idol(this.name, this.members)
}

<결과>
false
void main() {
	Idol bts1 = const Idol(
    	"BTS",
        ["RM","진","슈가","제이홉","지민","뷔","정국"]
    );
    Idol bts2 = const Idol(
    	"BTS",
        ["RM","진","슈가","제이홉","지민","뷔","정국"]
    );
    
    print(bts1 == bts2)
}

class Idol {
  final String name;
  final List<String> members;

  const Idol(this.name, this.members)
}

<결과>
true

getter, setter

getter는 인스턴스의 변수를 가져오는 것이고 setter는 인스턴스의 변수 값을 변경해준다.
getter는 파라미터를 사용하지 않는다.
setter는 파라미터에 무조건 하나의 값만 들어간다.

void main() {
  Idol bts = Idol(
    "BTS", 
    ["RM", "진", "슈가", "제이홉", "지민", "뷔", "정국"]
  );
  
  // getter 사용
  print(bts.firstMember);
  
  // setter 사용
  bts.firstMember = "아이언맨";
  print(bts.firstMember);
}

class Idol {
  String name;
  List<String> members;

  Idol(String name, List<String> members)
      : this.name = name,
        this.members = members;

  // getter
  String get firstMember {
    return this.members[0];
  }

  // setter
  set firstMember(String name) {
    this.members[0] = name;
  }
}
<결과>
RM
아이언맨

getter와 setter를 사용하는 것은 늬앙스의 차이로 내가 해당 데이터를 간단하게 가공하고 싶다, 함수는 내가 특정한 로직을 사용하고 싶다 이럴때 쓰는데 취향차이인듯
setter는 요즘 immutable하게 짜는게 대세이기에 잘 사용하지 않는다.

private

private는 해당 파일 이외의 외부 파일에서 사용할 수 없게 만들기 위한 것이다.
클래스를 private하게 만들려면 클래스 앞에 _언더바를 넣으면 된다.
변수,함수도 언더바를 넣으면 된다.

void main() {
  _Idol bts = _Idol(
    "BTS", 
    ["RM", "진", "슈가", "제이홉", "지민", "뷔", "정국"]
  );
}

class _Idol {
  String name;
  List<String> members;

  _Idol(String name, List<String> members)
      : this.name = name,
        this.members = members;
}

상속

상속을 받으면 부모 클래스의 모든 속성을 자식 클래스가 부여받는다.
부모 클래스를 상속받고 싶으면 자식 클래스를 만들때 extends 부모클래스를 적어서 상속받을 수 있다.

class BoyGroup extends Idol{}

그런데 상속받을 때, 부모 클래스가 정해둔 생성자를 준수해주어야된다.

class Idol {
  String name;
  int membersCount;
  Idol({
   	required this.name,
    required this.membersCount,
  });
}

class BoyGroup extends Idol {
  BoyGroup(
    String name, 
    int membersCount
  ): super(
        name: name, 
        membersCount: membersCount
      );
}

그럼 그냥 사용하면 된다.
이렇게 되면 BoyGroup에서 name과 membersGroup를 받으면 해당 변수들이 다시 그의 super인 Idol로 넘어가는 방식이다.
BoyGroup에 다른 거를 선언안했어도 Idol을 상속 받았기에 Idol에 있는 메소드인 sayName과 sayMembersCount를 사용할 수 있다.

void main() {
  BoyGroup BTS = BoyGroup("BTS", 7);
  BTS.sayMembersCount();
  BTS.sayName();
}

그런데 BoyGroup에 따로 선언된 변수나 메소드는 그의 부모 클래스가 사용할 수 없다. 여기서 sayMale은 BoyGroup로 만들어진 인스턴스는 사용가능하지만 그 부모클래스인 Idol로 만들어진 인스턴스는 사용할 수 없다.

class BoyGroup extends Idol {
  BoyGroup(
    String name, 
    int membersCount
  ): super(
        name: name, 
        membersCount: membersCount
      );
  
  void sayMale() {
    print("저는 남자아이돌입니다.");
  }
}

그리고 새로운 Idol을 상속받는 클래스를 만들어서 거기에 메소드를 만들더라도 그 메소드를 BoyGroup는 사용할 수 없다. 자식클래스끼리도 서로 공유하지 않는다.

그리고 BTS가 BoyGroup을 받았다면 BTS는 Idol이면서 BoyGroup이지만 GirlGroup은 아니다.
twice가 Idol을 받았다면 twice는 Idol이지만 BoyGroup이 아니고 GirlGroup도 아니다.

Override

override - 덮어쓰다
기존의 부모가 쓰던 메소드를 덮어쓰고 싶을 때 사용
사용하고자 하는 메소드 위에 @override를 붙인다.
super.number를 쓸 때, this.number도 되고 number도 된다. 그렇지만 super가 보기는 편하지 않을까?

class TimesTwo {
  final int number;

  TimesTwo(
    this.number,
  );

  int calculate() {
    return number * 2;
  }
}

class TimesFour extends TimesTwo {
  TimesFour(
    int number,
  ) : super(number);
  
  @override
  int calculate(){
    return super.number * 4;
  }
}

해당 방식처럼 메소드를 불러오는 것도 가능하다. 그러나 이때에 this.calculate를 쓰게 된다면 자식 클래스의 claculate를 불러오게 되는 것이므로 무한 반복이 되기에 super를 써주어야 한다.

@override
  int calculate(){
    return super.calculate * 2;
  }

static

static은 인스턴스가 아닌 클래스에 귀속된다.
static을 선언한 변수 혹은 메소드는 클래스에 귀속되서 해당 클래스의 변수를 바꿔주면 다른 인스턴스는 해당 변수가 변경된 것에 모두 영향 받는다.
static이 사용된 변수 혹은 메소드에서 seulgi.building 이렇게 인스턴스에서 변환하는 것은 안된다. 해당 클래스.메소드 or 변수 이렇게 선언되어야 한다.

void main() {
  Employee seulgi = Employee("슬기");
  Employee chorong = Employee("초롱");
  seulgi.NameAndBuilding();
  chorong.NameAndBuilding();
  
  Employee.building = "오투타워";
  
  seulgi.NameAndBuilding();
  chorong.NameAndBuilding();
}

class Employee {
  static String? building;
  final String name;

  Employee(this.name);

  void NameAndBuilding() {
    print("제 이름은 $name입니다. $building 건물에서 근무하고 있습니다.");
  }

  static void Building() {
    print("$building에서 근무중입니다.");
  }
}
<결과>
제 이름은 슬기입니다. null 건물에서 근무하고 있습니다.
제 이름은 초롱입니다. null 건물에서 근무하고 있습니다.
제 이름은 슬기입니다. 오투타워 건물에서 근무하고 있습니다.
제 이름은 초롱입니다. 오투타워 건물에서 근무하고 있습니다.

인터페이스

다른 언어들은 interface라는 별도의 선언을 하지만 Dart는 그냥 class로 선언한다.
인터페이스는 해당 형태를 강제하라는 느낌이다.
인터페이스를 사용하고 싶으면 implements를 사용하면 된다. 그렇게 되면 implements받는 클래스는 하는 클래스의 형태를 무조건 따라야 한다.
여기서 아이돌인터페이스는 그냥 이런 형태로 나타낸다는 것을 의미하기에 별도의 추가 정의는 이루어지지 않는다.
상속은 속성과 기능을 물려주기 위해서 사용한다면, 인터페이스는 특수한 구조를 강제하기 위해서 사용한다.

class IdolInterface {
  String name;
  
  IdolInterface(this.name);
  
  void sayName() {}
}

class BoyGroup implements IdolInterface{
  String name;
  
  BoyGroup(this.name);
  
  void sayName() {}
}

만들어진 인터페이스가 실수로 사용되는 것을 방지하기 위해서 abstract를 사용하고 그렇게 되면 해당 인터페이스 안의 메소드의 body{}를 지워도 된다.

abstract class IdolInterface {
  String name;
  
  IdolInterface(this.name);
  
  void sayName(); 
}

그리고 상속처럼 implements한 것들도 타입비교가 가능하다. 즉 보이그룹은 아이돌인터페이스이면서 보이그룹이지만 아이돌인터페이스는 보이그룹이 될 수 없다.

generic

타입을 외부에서 받을 때 사용
T는 그냥 자기가 넣고 싶은 이름 넣으면 된다.
여러개 선언하고 싶으면 <t,x,w,...>이렇게 하고 나중에 선언할 때 다 넣어주기만 하면 된다.

void main() {
  Lecture<String> lecture1 = Lecture("123","lecture");
  lecture1.printIdType();
  
  Lecture<int> lecture2 = Lecture(123,"lecture");
  lecture2.printIdType();
}

class Lecture<T> {
  final T id;
  final String name;
  
  Lecture(this.id, this.name);
  
  void printIdType(){
    print(id.runtimeType);
  }
}
<결과>
String
int
profile
아직 대학생입니다

0개의 댓글