8. Структуры, перечисления и варианты

Узнайте о полях, инициализации структуры и роли enum и variant.

Wave Foundation

Выражение отношений между данными как типами

Если цена и количество продуктов передаются как переменные, просто взглянув на код, трудно определить, принадлежат ли эти два значения одному и тому же продукту. Структура группирует связанные поля. enum представляет именованное состояние, а variant хранит разные данные вместе для каждого случая.

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

что-то выразить выбрать да
Несколько полей существуют одновременно struct Цена за единицу и количество продукта
Именованное целочисленное состояние enum Подождать/Продолжить/Завершить
В каждом случае разные данные variant значение успеха или ошибка

Декларация структуры и создание стоимости

struct Product {
    price: i32;
    quantity: i32;
}

fun main() {
    var item: Product = Product {
        price: 1500,
        quantity: 2
    };

    println("{} {}", item.price, item.quantity);
}

Результат выполнения:

1500 2

Поля в объявлениях заканчиваются точкой с запятой, а при создании значений поля и значения соединяются двоеточиями и разделяются запятыми. Определение типа и создание фактического значения — это два разных этапа. Объявление типа Product не создает автоматически место для хранения одного продукта.

Доступ к полю осуществляется как item.price. Если вы создадите несколько item одного типа, вы сможете хранить в каждом разные значения.

Передача структуры в функцию

struct Product {
    price: i32;
    quantity: i32;
}

fun subtotal(item: Product) -> i32 {
    return item.price * item.quantity;
}

fun main() {
    var item: Product = Product {
        price: 1500,
        quantity: 2
    };

    println("{}", subtotal(item));

    item.quantity = 3;
    println("{}", subtotal(item));
}

Результат выполнения:

3000
4500

Функция получает в качестве типа связь, согласно которой цена за единицу и количество принадлежат одному и тому же продукту. Это функция, которая считывает структуру целочисленного поля, переданную как значение. Любая функция, которая хочет изменить память вызывающего объекта, может быть спроектирована так, чтобы принимать указатель.

Если в структуре есть поля-указатели, при копировании значения также копируется адрес. Это не функция глубокого копирования в отдельные выделения. Типы, содержащие ресурсы, такие как дескрипторы файлов или Buffer, должны вместе указывать правила копирования и выпуска.

Метод и proto

Связанные функции могут быть сгруппированы в форме метода. proto — способ записи метода структуры в виде отдельного блока.

struct Counter {
    value: i32;
}

proto Counter {
    fun current(self: Counter) -> i32 {
        return self.value;
    }
}

fun main() {
    var counter: Counter = Counter {
        value: 3
    };

    println("{}", counter.current());
}

Результат выполнения:

3

self: Counter — параметр, которому присваивается значение. Использование нотации вызова метода не делает его автоматически методом, изменяющим оригинал. Пожалуйста, прочитайте вместе тип self и что он делает в тексте.

Вы не можете ожидать, что поле будет иметь только допустимое состояние только потому, что вы прикрепили к нему метод. Если существуют недопустимые комбинации, которые пользователь может создать с помощью общедоступных полей, ваша функция должна проверить их наличие или предоставить правило создания.

Назовите штат с помощью enum.

enum State -> i32 {
    Ready = 0,
    Running,
    Finished,
}

fun main() {
    var state: State = State::Ready;

    if (state == State::Ready) {
        println("ready");
    }

    state = State::Running;

    if (state == State::Running) {
        println("running");
    }
}

Результат выполнения:

ready
running

-> i32 — целочисленный тип, используемый в выражениях. Первое значение установлено равным 0, а последующие пропущенные значения на 1 больше предыдущего значения. Вместо того, чтобы просто сравнивать 0 и 1 в вашем коде, используйте State::Ready и State::Running, чтобы раскрыть смысл.

enum Наличие имени не ограничивает автоматически переходы между состояниями. Такие правила, как возможность возврата из Finished в Running, должны быть реализованы как функции.

Соединение случаев и данных с помощью variant

Если значение существует только в случае успеха, а в случае неудачи необходима информация об ошибке, ее можно выразить как variant.

variant Result {
    Value(i32),
    Error(i32)
}

fun divide(left: i32, right: i32) -> Result {
    if (left < 0 || right <= 0) {
        return Result::Error(1);
    }

    return Result::Value(left / right);
}

fun show(result: Result) {
    match (result) {
        Result::Value(value) => {
            println("value={}", value);
        }
        Result::Error(code) => {
            println("error={}", code);
        }
    }
}

fun main() {
    show(divide(12, 3));
    show(divide(12, 0));
}

Результат выполнения:

value=4
error=1

Result::Value и Result::Error каждый содержит payload. Даже если они содержат один и тот же целочисленный тип, определенные случаи различаются. Вызывающий проверяет регистр с помощью match и использует payload внутри этого arm.

divide выше — небольшой пример, который не поддерживает отрицательные операнды. Поскольку указан диапазон ввода, его не следует путать с функцией, охватывающей все границы обычного деления signed.

Если payload нет

Данные требуются не во всех случаях. Состояние отсутствия ценности можно выразить как отдельный случай.

variant Lookup {
    Found(i32),
    Missing
}

fun main() {
    var result: Lookup = Lookup::Missing;

    match (result) {
        Lookup::Found(value) => {
            println("found={}", value);
        }
        Lookup::Missing => {
            println("missing");
        }
    }
}

Результат выполнения:

missing

Вместо того, чтобы зарезервировать одно произвольное целое число как «none», мы использовали вариант Missing. Независимо от того, какова ценность успеха, значения не пересекаются.

match–_ обрабатывают остальные случаи. Если вы хотите, чтобы каждый вызывающий абонент проверялся снова при добавлении нового обращения, лучше явно разделить все обращения. Какой бы метод вы ни выбрали, убедитесь, что нет необработанных входных данных.

Использование структуры и variant вместе

Выбор разных данных можно выразить как variant, а группировку нескольких полей, принадлежащих одному случаю, можно выразить как структуру. Например, если результат обработки заказа успешный, он может содержать структуру поступления, а если результат неудачный, он может содержать номер ошибки.

Правило времени жизни памяти не исчезает, даже если значение содержит другие значения. Если указатель хранится в variant, то, действителен ли указатель и кто его освободит, определяется отдельно. Даже при сохранении во внешнем формате файла необходимо указать кодировку для каждого поля, а не выгружать структурную память как есть.

Упражнение и полное решение

Создайте вердикт, который принимает только оценки от 0 до 100. Действительные оценки выражаются как Grade(score), остальные выражаются как Invalid.

variant CheckedScore {
    Grade(i32),
    Invalid
}

fun check_score(score: i32) -> CheckedScore {
    if (score < 0 || score > 100) {
        return CheckedScore::Invalid;
    }

    return CheckedScore::Grade(score);
}

fun main() {
    var result: CheckedScore = check_score(87);

    match (result) {
        CheckedScore::Grade(score) => {
            println("accepted={}", score);
        }
        CheckedScore::Invalid => {
            println("invalid score");
        }
    }
}

Результат выполнения:

accepted=87

Измените ввод на -1, 0, 100, 101, чтобы проверить границы. Поскольку успех и неудача не используют одно и то же целочисленное пространство, вызывающая сторона снижает риск случайного добавления значений ошибок к среднему вычислению.

Какое выражение мне выбрать?

форма данных подходящее выражение да
Несколько значений одного типа Массив 10 баллов
Несколько полей, связанных друг с другом структура имя и оценка
Именованное значение состояния enum Ready, Running, Stopped
Дополнительные данные, которые различаются в зависимости от штата variant Value(i32), Error(str)
Контекстное имя существующего типа введите псевдоним UserId = u64

При выборе структуры данных учитывайте не только значения, которые вы хотите хранить, но и то, какие ложные состояния вы можете представлять. Структура с полями успеха/неудачи и значения/ошибки может создать неправильную комбинацию, но variant можно дифференцировать на payload для каждого случая.

Размещение памяти и внешние данные

Память структуры может содержать пустое пространство для обеспечения выравнивания между полями. Простое сложение размеров полей не всегда равно общему размеру структуры. Если вам нужно узнать размер и выравнивание, используйте mem Функция макета.

Для файлов или сетевых сообщений порядок и длину байтов можно четко определить путем кодирования полей по порядку с помощью функции bytes. При передаче структур на другие языки согласуйте внешнее объявление FFI с целью ABI.

Структура обучения и практики · variant