Pull to refresh

Использование wildcard-переменной _ в Dart 3

Reading time7 min
Views959

С выходом Dart 3 в язык были добавлены значительные нововведения, включая рекорды, паттерн-матчинг и модификаторы классов. Данная статья посвящена менее обсуждаемой, но полезной возможности, улучшающей чистоту и выразительность кода — wildcard-переменной, обозначаемой символом _.

Символ _ в определенных контекстах позволяет явно указать на намерение разработчика проигнорировать некоторое значение. Рассмотрим сценарии использования и преимущества этого механизма.

Проблема: Неиспользуемые переменные

Разработчики часто сталкиваются с ситуациями, когда необходимо объявить переменную, значение которой не используется в дальнейшем коде, например:

  1. Параметры колбэков:

    future.then((value) { // Параметр 'value' объявлен согласно сигнатуре
      print('Операция завершена!');
      // 'value' не используется в теле колбэка
    }).catchError((error, stackTrace) { // Параметр 'stackTrace' может быть не нужен
      _logError(error);
      // 'stackTrace' не используется
    });
    
  2. Индекс при итерации (если нужен только элемент):

    final List<String> items = ['яблоко', 'банан', 'апельсин'];
    items.asMap().forEach((index, item) { // 'index' объявляется
      print('Фрукт: $item');
      // 'index' не используется
    });
    
  3. Частичная деструктуризация:

    (String, int, bool) fetchUserInfo() => ('Alice', 30, true);
    
    final (String name, int age, bool isActive) userInfo = fetchUserInfo();
    // Требуется только 'age'
    final userAge = userInfo.$2;
    print('Возраст пользователя: $userAge');
    // 'name' и 'isActive' объявлены, но не используются
    

Во всех перечисленных случаях статический анализатор (линтер) может генерировать предупреждения (unused_local_variable, unused_element). Игнорирование этих предупреждений или использование имен переменных вроде unused не решает проблему по существу и может ухудшать читаемость кода.

Решение: Wildcard _

Wildcard _ представляет собой специальный синтаксис, который информирует компилятор и других разработчиков о том, что значение в данном месте намеренно игнорируется.

Ключевые особенности _:

  1. Не создает переменную (в контексте паттернов): При использовании _ внутри паттернов (деструктуризация, switch, if-case) Dart не создает локальную переменную. Выполняется проверка соответствия значения паттерну, но само значение не сохраняется. Это может потенциально снизить потребление памяти для игнорируемых объектов.

  2. Многократное использование: можно использовать несколько раз в одной области видимости или паттерне без конфликтов имен. Каждый экземпляр функционирует как независимый маркер игнорирования.

  3. Подавление предупреждений линтера: Применение _ для игнорируемых значений является семантически корректным способом избежать предупреждений об неиспользуемых переменных.

Рассмотрим обновленные примеры с использованием _.

1. Игнорирование параметров в колбэках

// Пример с forEach:
items.asMap().forEach((_, item) { // Использование '_' для индекса
  // Указывает, что индекс не требуется, используется только значение.
  print('Фрукт: $item');
});

// ------

// Пример с Future:
Future<String> fetchData() async { /* ... */ return "Data"; }
Future<void> saveToDb() async { /* ... */ }
Future<void> cacheLocally() async { /* ... */ }

Future<void> processData() async {
  // Использование Future.wait, результаты отдельных Future не важны.
  await Future.wait([
    saveToDb(),
    cacheLocally(),
  ]).then((_) { // Использование '_' для списка результатов Future.wait
    // Сигнализирует, что конкретные результаты не интересуют, важен факт завершения.
    print('База данных и кэш обновлены!');
  });

  final result = await fetchData();
  await saveToDb().then((_) { // Результат saveToDb() (void) игнорируется.
     print('Данные сохранены, результат $result');
  });
}

Объяснение: В примере с forEach указывает на игнорирование индекса. В примере с Future.wait в then используется для игнорирования списка результатов, так как важно только завершение всех операций.

2. Итерация по Map без ключа (или без значения)

final Map<String, int> scores = {'Alice': 100, 'Bob': 85, 'Charlie': 92};

// Обработка только значений (очков)
print('Обрабатываем очки:');
for (final (_, score) in scores.entries) { // Использование '_' для ключа
  // При деструктуризации MapEntry ключ игнорируется.
  // Это лаконичнее, чем доступ через entry.value.
  print('Получено очков: $score');
}

// Обработка только ключей (имен)
print('\nОбрабатываем имена:');
for (final (name, _) in scores.entries) { // Использование '_' для значения
  // Аналогично, игнорируется значение.
  print('Игрок: $name');
}

Объяснение: При итерации по scores.entries деструктуризация (key, value) в цикле for-in позволяет с помощью _ сразу отбросить ненужную часть (key или value), фокусируясь на используемых данных.

3. Деструктуризация с игнорированием

_ эффективен при работе со структурами данных, такими как списки и рекорды.

Списка:

final List<int> coordinates = [10, 20, 30, 40, 50];

// Получение второго и третьего элементов
final [_, y, z, ...] = coordinates; // '_' для первого, '...' для остальных после z
// Указывает на игнорирование первого элемента и всех после третьего.
print('Координаты YZ: ($y, $z)'); // Вывод: Координаты YZ: (20, 30)

// Получение первого и последнего
final [first, ..., last] = coordinates; // '...' игнорирует элементы между первым и последним
print('Первый: $first, Последний: $last'); // Вывод: Первый: 10, Последний: 50

Рекордов (Records):

(int, int, int) getPixelColor() => (255, 170, 187); // RGB

// Получение зеленого (G) и синего (B) каналов
final (_, g, b) = getPixelColor(); // '_' для красного (R) канала
// Код явно показывает игнорирование красного канала.
print('Зеленый: $g, Синий: $b'); // Вывод: Зеленый: 170, Синий: 187

Объяснение: При деструктуризации _ позволяет пропустить ненужные элементы структуры данных, делая намерение игнорировать часть данных более очевидным.

4. Паттерн-матчинг в switch и if-case

_ является важным элементом синтаксиса паттерн-матчинга, позволяя сопоставлять "любое значение" или служить аналогом default.

String checkPair(Object record) {
  return switch (record) {
    // Сопоставление рекорда (пары): первый элемент - int, второй - любой (игнорируется)
    (int count, _) => 'Найдено $count элементов.',
    // Сопоставление рекорда: первый элемент - любой (игнорируется), второй - String
    (_, String label) => 'Найдена метка "$label".',
    // Сопоставление по типу, значение не важно
    bool _ => 'Это просто булево значение.',
    // Wildcard для остальных случаев
    _ => 'Неизвестная пара или тип.'
  };
}

print(checkPair((10, 'items'))); // Вывод: Найдено 10 элементов.
print(checkPair((null, 'User'))); // Вывод: Найдена метка "User".
print(checkPair(true));         // Вывод: Это просто булево значение.
print(checkPair([1, 2]));      // Вывод: Неизвестная пара или тип.

// ------

// Пример валидации JSON-подобной структуры Map
void processJson(Map<String, dynamic> json) {
  switch (json) {
    // Поиск Map с ключом 'id' (int) и 'data' (любой тип, значение игнорируется)
    case {'id': int id, 'data': _}: // Игнорирование значения по ключу 'data'
      // Указание на то, что требуется только 'id', а значение 'data' не используется.
      // Это может оптимизировать обработку, так как значение 'data' не извлекается.
      print('Обрабатываем запись с ID: $id');
      handle(id);
      break;
    case {'error': String message}:
      print('Получена ошибка: $message');
      break;
    default:
      print('Неизвестный формат JSON: $json');
      throw FormatException('Неверный формат JSON');
  }
}

void handle(int id) { /* ... */ }

processJson({'id': 123, 'data': {'value': 42}}); // Вывод: Обрабатываем запись с ID: 123
// ... (остальные вызовы processJson и обработка исключения)

Объяснение: В switch позволяет создавать гибкие паттерны. В case (int count, ) сопоставляется пара, где важен только первый элемент. В примере с JSON case {'id': int id, 'data': _} демонстрирует проверку наличия ключа data без извлечения и использования его значения.

Различие между и variableName

Необходимо различать wildcard и имя переменной, начинающееся с (например, _myPrivateVar).

  • (Wildcard в паттернах): Внутри паттернов является специальной конструкцией, которая не создает переменную и не хранит значение. Попытка чтения из _ в этом контексте приведет к ошибке компиляции.

  • (Вне паттернов): При использовании вне паттернов (например, var = someValue;), формально может вести себя как переменная с именем . Однако такая практика не рекомендуется. Современные анализаторы кода препятствуют чтению значения из такой переменной.

  • variableName: Это обычная переменная. Префикс делает ее приватной для текущей библиотеки (файла), согласно конвенции Dart. Значение такой переменной можно читать и изменять. Линтер выдаст предупреждение, если она не используется.

Рекомендация: Используйте исключительно для обозначения игнорируемого значения. Для переменных, включая приватные, используйте осмысленные имена (internalCounter).

Правило линтера: Правило discard_Namen (включено по умолчанию в рекомендуемых наборах правил для Dart 3) запрещает чтение из переменной с именем _, поощряя использование этого символа строго для игнорирования значений.

void checkContext(dynamic value) {
  // Корректное использование в паттерне:
  if (value case [int count, _]) { // Игнорирование второго элемента
    print('List starts with count: $count');
    // print(_); // Ошибка компиляции
  }

  // Использование вне паттерна (чтение не рекомендуется):
  var _ = value;
  // print(_); // Предупреждение линтера `discard_Namen`

  // Обычная приватная переменная:
  var _internalState = value;
  print('Internal state: $_internalState'); // Корректно
}

Преимущества использования _

  1. Повышение читаемости: Код становится яснее, так как намерение игнорировать значение выражено явно.

  2. Четкое выражение намерения: _ служит сигналом о том, что значение не требуется для дальнейшей логики.

  3. Уменьшение "шума": Сокращается количество неиспользуемых имен переменных.

  4. Поддержка паттерн-матчинга: _ является неотъемлемой частью синтаксиса паттернов.

  5. Совместимость с линтером: Позволяет избежать предупреждений анализатора для намеренно неиспользуемых значений.

  6. Потенциальная оптимизация: Компилятор может избегать выделения ресурсов для значений, помеченных как игнорируемые в паттернах.

Заключение

Wildcard-переменная в Dart 3 является полезным инструментом для написания более чистого и поддерживаемого кода. Она позволяет явно указывать на игнорируемые значения в различных контекстах, от колбэков до паттерн-матчинга, улучшая общую читаемость и выразительность кода. Рекомендуется использовать в соответствии с его назначением — для игнорирования значений, не предназначенных для дальнейшего использования.

Tags:
Hubs:
Total votes 4: ↑4 and ↓0+6
Comments0

Articles