본문 바로가기
Dart (Flutter)

[Dart] Classes

by llHoYall 2023. 2. 8.

Dart는 class와 mixin 기반의 상속을 지원하는 OOP 언어입니다.

따라서, 이번 포스팅에서 소개드릴 class 관련 내용이 핵심이라고도 볼 수 있습니다.

용어가 조금 혼용되는 느낌이 있는 것 말곤 타 언어대비 특별할 건 딱히 없어서 편하게 보실 수 있을거에요.

Dart에서 Null을 제외한 모든 object는 Object로부터 상속됩니다.

Constructors

생성자는 Class 이름과 동일하게 만들거나, named constructor로 만들 수 있습니다.

아무런 생성자도 정의하지 않는다면, parameter를 받지않는 default constructor가 자동으로 제공됩니다.

생성자는 상속되지 않습니다.

Instance의 생성 시, new keyword는 생략이 가능합니다.

간단한 예제와 함께 보세요.

class Point {
  int x;
  int y;

  Point(this.x, this.y);
  Point.fromJson({int x = 0, int y = 0})
      : x = x,
        y = y;
}

void main() {
  var p1 = Point(2, 2);
  var p2 = Point.fromJson(x: 1, y: 2);
}

Named parameter도 사용 가능함을 보여드리려고 named constructor에 적용해보았습니다.

Redirecting Constructors

Constructor를 redirecting 시킬 수 있습니다.

class Point {
  int x;
  int y;

  Point(this.x, this.y);
  Point.fromJson(int x) : this(x, 0);
}

Named constructor가 호출되면 다른 생성자가 호출되도록 redirecting 시킨 예제입니다.

Constant Constructors

Constant constructor를 만들기 위해서는 const로 선언을 하고, 다른 모든 instance variable들은 final로 선언해야 합니다.

Constant constructor로 instance를 만들면 compile-time 상수가 됩니다.

즉, 동일한 argument로 생성하면 동일한 instance가 생성됩니다.

class ImmutablePoint {
  final int x;
  final int y;

  const ImmutablePoint(this.x, this.y);
}

void main() {
  var p1 = const ImmutablePoint(2, 2);
  var p2 = const ImmutablePoint(2, 2);
  print(identical(p1, p2));  // true
}

Factory Constructors

Factory constructorfactory keyword로 만들며, new keyword를 사용하지 않고 instance를 만들 수 있습니다.

Class의 instance를 반환하거나, sub-type의 instance를 반환합니다.

이 때, 새 instance를 생성하는 대신 이전에 생성되었거나 cache에서 가져온 instance를 반환합니다.

class Point {
  num? x;
  num? y;

  Point(this.x, this.y);

  factory Point.fromJson(Map<String, num> json) {
    return Point(json['x'], json['y']);
  }
}

void main() {
  var p1 = Point.fromJson({'x': 1, 'y': 2});
  var p2 = Point.fromJson({'x': 3, 'y': 4});
}

Invoking Superclass Constructor

super keyword를 사용하여 부모 class의 생성자를 호출할 수 있습니다.

class Person {
  String? name;
  int? age;

  Person.fromJson(Map data) {
    print('Person Class');
  }
}

class Student extends Person {
  Student.fromJson(super.data) : super.fromJson() {
    print('Student Class');
  }
}

void main() {
  var student = Student.fromJson({});
  print(student);
}

: super.fromJson() 부분을 super-initializer라고 하는데, 자식의 생성자가 호출될 때 부모의 생성자가 먼저 호출됩니다.

 

간접적으로 부모의 생성자를 호출할 수도 있습니다.

class Vector2D {
  final double x;
  final double y;

  Vector2D(this.x, this.y);
}

class Vector3D extends Vector2D {
  final double z;

  Vector3D(super.x, super.y, this.z);
}

void main() {
  var vector = Vector3D(1, 2, 3);
}

위 예제에서는 3개의 입력을 받아, 2개는 super keyword를 붙여 부모의 생성자로 보냅니다.

 

Named parameter를 적용할 수도 있습니다.

class Vector2D {
  ...
  Vector2D.named({required this.x, required this.y});
}

class Vector3D extends Vector2D {
  ...
  Vector3D.named({required super.y, required this.z}) : super.named(x: 0);
}

Instance Variables

Instance variable은 type을 명시해야합니다.

class Point {
  int x = 0;
  int? y;  // initially null
}

모든 instance variable은 암묵적으로 getter method를 생성합니다.

또한, non-final과 late final instance variable들은 암묵적으로 setter method를 생성합니다.

class Point {
  int x = 0;
  int? y;  // initially null
}

void main() {
  var point = Point();
  point.x = 3;  // setter
  point.y = 2;  // setter
  print("X: ${point.x}, Y: ${point.y}");  // getter
}

Methods

Instance Methods

Instance method는 instance variable에 접근할 수 있습니다.

import 'dart:math';

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var p1 = Point(1, 2);
  var p2 = Point(2, 4);
  print(p1.distanceTo(p2));
}

Operator Overriding

연산자를 overriding해서 원하는대로 변경할 수 있습니다.

변경이 가능한 연산자는 다음과 같습니다.

<, >, <=, >=, -, +, /, ~/, *, %, |, ^, &, <<, >>, >>>, [], []=, ~, ==

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  @override
  bool operator ==(Object other) =>
      other is Vector && x == other.x && y == other.y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  final v1 = Vector(2, 3);
  final v2 = Vector(2, 2);
  
  print("${(v1 + v2).x}, ${(v1 + v2).y}");
  print(v1 - v2 == Vector(0, 1));
  print(v1.hashCode);
}

getter and setter

각각 get, set keyword를 사용해서 만들 수 있습니다.

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  double get right => left + width;
  set right(double value) => left = value - width;
}

void main() {
  var rect = Rectangle(1, 2, 2, 4);
  print(rect.right);  // getter
  rect.right = 12;  // setter
}

Abstract Class

추상 class는 abstract modifier로 정의하며, instance를 만들 수 없습니다.

Interface를 정의할 때, 유용하게 사용할 수 있습니다.

Method도 interface만 정의하고 구현은 상속받는 class에게 맡기는 abstract method를 주로 갖습니다.

abstract class Super {
  void doSomething();
}

class Sub extends Super {
  void doSomething() {
    // Implementing
  }
}

Inheritance

상속을 class를 확장한다고 표현하며, extends keyword를 사용합니다.

자식 class에서 부모 class의 method를 overriding할 수 있습니다.

이 때, override keyword로 명시를 해야하며 return type은 동일해야 합니다.

Argument type은 같거나 상위 type 이어야 합니다.

부모의 positional parameter는 모두 받아야 합니다.

또, generic method와 non-generic method 간에는 override가 불가합니다.

존재하지 않는 instance variable이나 method를 사용했을 때, 검출하거나 처리하려면 noSuchMethod()를 overide해야 합니다.

class Super{
  void method1() { ... }
  void method2(int param) { ... }
}

class Sub extends Super {
  void method1() {
    super.method();
  }

  @override
  void method2(num param) { ... }

  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ${invocation.memberName}');
  }
}

Class Variables and Methods

static keyword를 사용하여 class 범위의 variable 혹은 method를 구현할 수 있습니다.

Static variable과 static method에 대해 각각 간단한 예제로 설명을 대신하겠습니다.

Static Variables

class Rectabgle {
  static const vertices = 4;
}

void main() {
  assert(Rectabgle.vertices == 4);
}

Static Methods

import 'dart:math';

class Point {
  double x, y;

  Point(this.x, this.y);

  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
}

Implicit Interfaces

implements keyword로 interface를 구현할 수 있습니다.

하나의 class가 여러 interface를 구현하는 것도 가능합니다.

class Person implements Eat, Say { ... }

Mixins

Mixin은 class를 재사용하는 방법으로, 생성자를 갖지 않는 class 입니다.

mixinwith keyword를 사용하여 정의합니다.

mixin MixinExample {
  bool testVar = false;

  void testMe() {
    if (testVar ) {
      print('Test True');
    } else {
      print('Test False');
    }
  }
}

class Example with MixinExample { ... }

on keyword를 사용하여 mixin이 가능한 super class를 제한할 수도 있습니다.

class Super { ... }
mixin MixinA on Super { ... }
class Sub extends Super with MixinA { ... }

Wrap Up

이번에는 Dart의 핵심인 class에 관해 살펴보았습니다.

적당히 묶어서 설명을 시도해봤는데, 다른 언어를 통해 OOP가 이미 익숙하신 분들께는 괜히 쓸데없이 설명이 길었나 싶기도 하고, 낯선 분들께는 부족할 것 같기도 하네요.

모쪼록 도움이 되셨길 바랍니다.

'Dart (Flutter)' 카테고리의 다른 글

[Dart] Generics  (0) 2023.02.08
[Dart] Enumerated Type  (0) 2023.02.08
[Dart] Functions  (0) 2023.02.03
[Dart] Control Flow Statements  (0) 2023.02.03
[Dart] Operators  (0) 2023.02.02

댓글