← Назад к вопросам

Что такое Null Safety в Dart и зачем он нужен?

1.0 Junior🔥 231 комментариев
#Dart

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое Null Safety в Dart и зачем он нужен?

Null Safety — это система типов в Dart, которая гарантирует, что переменная не может содержать null, если это явно не указано. Это предотвращает NullPointerException и делает код более безопасным и надежным.

Проблема, которую решает Null Safety

Без Null Safety (старый код):

String name = fetchName();
print(name.length); // ❌ Может быть null!
// Результат: NoSuchMethodError

С Null Safety:

String? name = fetchName(); // ? указывает на возможность null
if (name != null) {
  print(name.length); // ✅ Безопасно
}

Основные концепции

1. Non-nullable типы (не могут быть null)

String name = 'John';     // ✅ Может содержать только строку
int age = 30;             // ✅ Может содержать только число
List<int> numbers = [1, 2, 3]; // ✅ Может содержать только числа

// ❌ Ошибка компиляции
String invalidName = null; // Type 'Null' can't be assigned to 'String'

2. Nullable типы (могут быть null)

String? name = null;      // ✅ Может содержать строку или null
int? age = null;          // ✅ Может содержать число или null
List<int>? numbers = null; // ✅ Может содержать список или null

// Присвоение значения
String? email = 'user@example.com'; // ✅ Работает
email = null;                        // ✅ Работает

Проверка на null (Null Coalescing)

1. Оператор ?. (safe navigation)

String? name = null;
int? length = name?.length; // null вместо ошибки
print(length); // null

2. Оператор ?? (null coalescing)

String? name = null;
String displayName = name ?? 'Unknown';
print(displayName); // 'Unknown'

// С значением
String? email = 'user@example.com';
String displayEmail = email ?? 'not provided';
print(displayEmail); // 'user@example.com'

3. if-let проверка

String? name = null;
if (name != null) {
  print(name.length); // ✅ name не может быть null здесь
}

// Или
String? name = 'John';
if (name case String n) { // Pattern matching
  print(n.length); // ✅ n точно String
}

Функции с Null Safety

1. Параметры функции

// Параметр должен быть string (не null)
void printName(String name) {
  print(name);
}

printName('John');  // ✅ OK
printName(null);    // ❌ Ошибка компиляции

// Параметр может быть null
void printAge(int? age) {
  if (age != null) {
    print('Age: $age');
  } else {
    print('Age: unknown');
  }
}

printAge(30);  // ✅ OK
printAge(null); // ✅ OK

2. Возвращаемые значения

// Функция ВСЕГДА возвращает String
String getName() {
  return 'John';
}

// Функция может вернуть String или null
String? getUserEmail() {
  return getEmailFromDatabase();
}

String? getPhoneNumber() {
  // Может быть null
  return null;
}

Практические примеры

Пример 1: Безопасное использование данных

class User {
  final String name;      // Не может быть null
  final String? email;    // Может быть null
  final int age;          // Не может быть null
  final String? phone;    // Может быть null
  
  User({
    required this.name,
    this.email,
    required this.age,
    this.phone,
  });
  
  String getContactInfo() {
    if (email != null) {
      return email!; // ! говорит: я уверен, это не null
    } else if (phone != null) {
      return phone!;
    } else {
      return 'No contact info';
    }
  }
}

// Использование
final user = User(
  name: 'John', // Обязательно
  age: 30,      // Обязательно
  email: 'john@example.com', // Опционально
);

Пример 2: Работа с API ответами

class ApiResponse<T> {
  final T? data;
  final String? error;
  final bool isSuccess;
  
  ApiResponse({
    this.data,
    this.error,
    required this.isSuccess,
  });
  
  T? getDataOrNull() => data;
  
  T getDataOrDefault(T defaultValue) {
    return data ?? defaultValue;
  }
}

// Использование
final response = ApiResponse<List<User>>(
  data: [User(...), User(...)],
  isSuccess: true,
);

final users = response.data; // List<User>?
if (users != null) {
  for (final user in users) {
    print(user.name);
  }
}

Пример 3: Widget с Null Safety

class UserCard extends StatelessWidget {
  final User user;        // Обязательно
  final Function()? onTap; // Опционально
  final Color? bgColor;    // Опционально
  
  const UserCard({
    required this.user,
    this.onTap,
    this.bgColor,
  });
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap, // null если не передан
      child: Card(
        color: bgColor ?? Colors.white, // Используем белый по умолчанию
        child: Column(
          children: [
            Text(user.name), // ✅ Безопасно, name не может быть null
            if (user.email != null) // Условный рендеринг
              Text('Email: ${user.email}'),
            if (user.phone case String phone) // Pattern matching
              Text('Phone: $phone'),
          ],
        ),
      ),
    );
  }
}

Оператор ! (force unwrap)

String? name = 'John';
int length = name!.length; // ! говорит: я уверен, это не null

// ❌ Будьте осторожны!
String? nullName = null;
int length = nullName!.length; // ❌ Выбросит ошибку в runtime!

Late переменные

// Для переменных, которые инициализируются позже
late String apiKey;

void initializeApp() {
  apiKey = 'secret123'; // Инициализация
}

void makeRequest() {
  // apiKey не может быть null благодаря late
  final headers = {'key': apiKey};
}

// ❌ Ошибка если использовать до инициализации
// void badCode() {
//   print(apiKey); // LateInitializationError
// }

Лучшие практики

1. Предпочитай non-nullable

// ❌ Слишком много nullable
class Product {
  String? name;
  String? description;
  double? price;
}

// ✅ Лучше
class Product {
  final String name;       // Обязательно
  final String description; // Обязательно
  final double price;       // Обязательно
  final String? discount;  // Опционально
}

2. Используй required параметры

// ❌ Плохо
class User {
  User(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

// ✅ Хорошо
class User {
  final String name;
  final int age;
  
  User({
    required this.name,
    required this.age,
  });
}

3. Обрабатывай null безопасно

// ❌ Рискованно
String? email = getEmail();
print(email!.length); // Может выбросить ошибку

// ✅ Безопасно
String? email = getEmail();
if (email != null) {
  print(email.length);
} else {
  print('Email not provided');
}

// Или
print(email?.length ?? 0); // 0 если email null

Миграция на Null Safety

Для старого кода:

// Было (без Null Safety)
String getName() {
  return null; // Допускалось в старых версиях
}

// Стало (с Null Safety)
String? getName() {
  return null; // ✅ Теперь явно указано
}

Преимущества Null Safety

Безопасность — предотвращает NullPointerException ✅ Четкость — явно видно какие значения могут быть null ✅ Оптимизация — компилятор лучше оптимизирует код ✅ Качество — меньше ошибок в production ✅ Читаемость — код понятнее что может быть null

Вывод

Null Safety — это не просто фишка Dart, это:

  • Революционный подход к безопасности типов
  • Предотвращение целого класса ошибок (null reference errors)
  • Обязательный стандарт для современного Dart/Flutter
  • Улучшение качества кода и производительности

Сегодня все новые Flutter проекты используют Null Safety и это стало стандартом разработки. Понимание и правильное использование Null Safety критично для написания качественного кода.

Что такое Null Safety в Dart и зачем он нужен? | PrepBro