Dart Clases y Objetos

Las clases son el concepto central de la programación orientada a objetos; son una descripción abstracta de las cosas del mundo real.

Los objetos son instancias concretas de una clase. Este capítulo presenta la definición de clases en Dart, los constructores, las variables miembro y métodos, getters/setters y los miembros estáticos.


Definición de clases e instanciación

Una clase es la plantilla para crear objetos; define qué atributos (datos) y comportamientos (métodos) tiene un objeto.

Ejemplo

// Define una clase User
class User {
  // Variables miembro (atributos)
  String name = '';
  int age = 0;

  // Métodos miembro (comportamientos)
  void introduce() {
    print('Hola, soy $name y tengo $age años.');
  }

  bool isAdult() {
    return age >= 18;
  }
}

void main() {
  // Instanciación: con la palabra clave new (en Dart 2.0+ se puede omitir new)
  var user1 = User();
  user1.name = 'ejemplo';
  user1.age = 10;

  // Acceder a variables miembro y métodos
  print('Usuario: ${user1.name}');
  user1.introduce();
  print('¿Es adulto?: ${user1.isAdult()}');

  // Crear otro objeto, independientes entre sí
  var user2 = User();
  user2.name = 'Xiaoming';
  user2.age = 20;
  user2.introduce();
}
用户名: ejemplo
你好,我是 ejemplo,今年 10 岁。
是否成年: false
你好,我是 小明,今年 20 岁。

Desde Dart 2.0, la palabra clave new es opcional al instanciar. La convención de la comunidad es omitir new para mantener el código limpio. Los ejemplos siguientes de este tutorial omitirán new.


Constructor

Un constructor es un método especial que se invoca automáticamente al crear un objeto, para inicializar su estado.

Dart ofrece múltiples formas de constructores, es muy flexible.

Constructor predeterminado

Si no defines un constructor, Dart proporciona automáticamente un constructor predeterminado sin parámetros.

Constructor ordinario

Ejemplo

class Student {
  String name;
  int age;

  // Constructor ordinario: tiene el mismo nombre que la clase
  Student(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void printInfo() {
    print('Estudiante: $name, edad: $age');
  }
}

void main() {
  var s1 = Student('ejemplo', 10);
  var s2 = Student('Xiaoming', 15);
  s1.printInfo();
  s2.printInfo();
}
学生: ejemplo, 年龄: 10
学生: 小明, 年龄: 15

Constructor de azúcar sintáctico

Cuando los parámetros del constructor se asignan directamente a variables miembro, Dart ofrece una sintaxis abreviada.

Ejemplo

class Student {
  String name;
  int age;

  // Azúcar sintáctico: this.nombreDelParámetro asigna automáticamente el parámetro a la variable miembro del mismo nombre
  // Esta forma es más concisa que escribir manualmente this.name = name
  Student(this.name, this.age);

  void printInfo() {
    print('Estudiante EJEMPLO: $name, edad: $age');
  }
}

void main() {
  var s = Student('ejemplo', 10);
  s.printInfo();
}
EJEMPLO 学生: ejemplo, 年龄: 10

Constructor nombrado

Una clase puede tener varios constructores nombrados, para diferentes lógicas de inicialización.

Ejemplo

class Point {
  double x;
  double y;

  // Constructor predeterminado
  Point(this.x, this.y);

  // Constructor nombrado: crea el origen
  Point.origin()
      : x = 0,
        y = 0;

  // Constructor nombrado: crea a partir de un solo valor (x e y son iguales)
  Point.diagonal(double value)
      : x = value,
        y = value;

  @override
  String toString() => 'Point($x, $y)';
}

void main() {
  var p1 = Point(3, 4);
  var p2 = Point.origin();
  var p3 = Point.diagonal(5);

  print('Coordenadas EJEMPLO: $p1, $p2, $p3');
}
EJEMPLO 坐标: Point(3.0, 4.0), Point(0.0, 0.0), Point(5.0, 5.0)

Lista de inicialización

La lista de inicialización se ejecuta antes del cuerpo del constructor; se usa para inicializar campos final o validar parámetros.

Ejemplo

class Person {
  final String name;    // Los campos final deben inicializarse en el constructor
  final int birthYear;
  final int age;

  // Lista de inicialización: después de los dos puntos, antes del cuerpo de la función
  // La lista de inicialización se ejecuta antes del cuerpo del constructor
  Person(this.name, this.birthYear)
      : age = 2026 - birthYear,          // Propiedad calculada
        assert(birthYear > 1900, 'El año de nacimiento no es válido');  // Validación con assert

  void printInfo() {
    print('Usuario EJEMPLO: $name, nacimiento: $birthYear, edad: $age');
  }
}

void main() {
  var p = Person('ejemplo', 2016);
  p.printInfo();

  // La siguiente línea hará fallar el assert (en modo de desarrollo)
  // var p2 = Person('error', 1800);
}
EJEMPLO 用户: ejemplo, 出生: 2016, 年龄: 10

assert solo está activo en modo de desarrollo (debug mode) y se ignora en el entorno de producción (release mode). Se utiliza para detectar errores lógicos lo antes posible durante el desarrollo.


Variables miembro y métodos

Las variables miembro almacenan el estado del objeto; los métodos miembro definen el comportamiento del objeto.

Ejemplo

class BankAccount {
  // Miembro privado: comienza con guion bajo, solo accesible dentro de este archivo
  String _accountNumber;
  double _balance = 0;

  // Miembro público
  String ownerName;

  BankAccount(this.ownerName, this._accountNumber);

  // Método público
  void deposit(double amount) {
    if (amount > 0) {
      _balance += amount;
      print('Depósito de ¥$amount, saldo actual: ¥$_balance');
    }
  }

  bool withdraw(double amount) {
    if (amount > 0 && amount <= _balance) {
      _balance -= amount;
      print('Retiro de ¥$amount, saldo actual: ¥$_balance');
      return true;
    }
    print('Saldo insuficiente, retiro fallido');
    return false;
  }

  // Método privado: solo para uso interno de la clase
  void _logTransaction(String type, double amount) {
    print('[Registro interno] $type: ¥$amount');
  }
}

void main() {
  var account = BankAccount('EJEMPLO', '6222-0000-1234');

  account.deposit(1000);
  account.withdraw(300);
  account.withdraw(800);  // Saldo insuficiente

  // El exterior no puede acceder a miembros privados
  // print(account._balance); // Error: _balance es privado
}
存入 ¥1000,当前余额: ¥1000
取出 ¥300,当前余额: ¥700
余额不足,取款失败

Getter y Setter

Los getters y setters son métodos especiales para operaciones de lectura y escritura de variables miembro.

Te permiten ejecutar lógica adicional al acceder a las propiedades, manteniendo una sintaxis limpia para el acceso a las mismas.

Ejemplo

class Circle {
  double _radius;  // Variable privada, almacena el radio

  Circle(this._radius);

  // Getter: obtiene el diámetro (propiedad calculada)
  double get diameter => _radius * 2;

  // Getter: obtiene el área
  double get area => 3.14159 * _radius * _radius;

  // Getter y Setter combinados: exponen la propiedad radius hacia el exterior
  double get radius => _radius;

  set radius(double value) {
    // En el Setter se puede añadir lógica de validación
    if (value <= 0) {
      throw ArgumentError('El radio debe ser mayor que 0');
    }
    _radius = value;
  }

  // Getter de solo lectura: no tiene Setter correspondiente
  String get info => 'Círculo(radio: $_radius)';
}

void main() {
  var circle = Circle(5);

  // Usar el Getter (como acceder a una propiedad normal)
  print('Círculo EJEMPLO: ${circle.info}');
  print('Diámetro: ${circle.diameter}');
  print('Área: ${circle.area.toStringAsFixed(2)}');

  // Usar el Setter
  circle.radius = 10;
  print('Diámetro después del cambio: ${circle.diameter}');

  // Intentar establecer un valor no válido lanza una excepción
  // circle.radius = -1; // Lanza ArgumentError
}
EJEMPLO 圆形: 圆(半径: 5.0)
直径: 10.0
面积: 78.54
修改后直径: 20.0

La ventaja de los getters y setters es que quien llama no necesita saber si detrás de radius hay un campo simple o una propiedad calculada. Si en el futuro necesitas añadir lógica de validación a radius, solo tienes que cambiar el campo por getter/setter, y el código que lo llama no necesita modificarse. Esta es la ventaja del "encapsulamiento".


Miembros estáticos

Los miembros estáticos pertenecen a la clase en sí, no a una instancia de la clase.

Todas las instancias comparten la misma variable estática; los métodos estáticos se pueden invocar sin crear un objeto.

Ejemplo

class Counter {
  // Variable estática: compartida por todas las instancias
  static int totalCount = 0;

  // Variable de instancia: independiente en cada instancia
  String label;

  Counter(this.label) {
    // Cada vez que se crea una instancia, el contador estático se incrementa en 1
    totalCount++;
  }

  // Método estático: se invoca mediante el nombre de la clase
  static void printTotal() {
    print('Número total de instancias EJEMPLO: $totalCount');
  }

  // Los métodos estáticos se usan a menudo para funciones de utilidad
  static bool isValidLabel(String label) {
    return label.isNotEmpty && label.length <= 20;
  }
}

void main() {
  // Método estático: se invoca directamente mediante el nombre de la clase, sin necesidad de crear una instancia
  print('¿Es válida la etiqueta?: ${Counter.isValidLabel('test')}');

  var c1 = Counter('Contador A');
  Counter.printTotal();  // 1

  var c2 = Counter('Contador B');
  var c3 = Counter('Contador C');
  Counter.printTotal();  // 3

  // Variable estática: se accede mediante el nombre de la clase
  print('Conteo final: ${Counter.totalCount}');
}
标签是否有效: true
EJEMPLO 总实例数: 1
EJEMPLO 总实例数: 3
最终计数: 3

Los métodos estáticos no pueden acceder a miembros de instancia (porque no hay this). Los métodos estáticos son adecuados para funciones de utilidad y lógica de fábrica, pero no debes abusar de ellos: los métodos estáticos no pueden ser sobrescritos por subclases, y su uso excesivo reduce la testabilidad y flexibilidad del código.

Otras extensiones