После небольшого перерыва продолжаем с управляющими последовательностями.

if/else Link to heading

В Python if как диалог с понимающим барменом вечером пятницы. Видит твой уставший взгляд, палец на негрони, кивок без лишних вопросов:

if user:
    send_email(user.email)

Пустой словарь? Значит нет пользователя. None? Значит нет пользователя. user = ""? Ну, получается, тоже пользователя не подвезли.

Когда проект небольшой такое поведение выглядит как забота и отсутствие overengineering’a. Когда проектик подрастает, то начинаешь ловить разнообразные спецэффекты с None, пустым словарем, словарем без email, строкой с Null, потому что парнерский сервис не прочитал контракт.

Rust же врывается на танцпол с бюрократией и требует четкие инструкции кто, когда и куда свернет.

В Python if синонимично попробуй угадай.

def greet(user):
    if user:
        print(f"Hello, {user['name']}")
    else:
        print("Hello, guest")

Rust такое не любит:

fn main() {
    let user = "";

    if user {
        println!("Hello");
    }
}

В Rust “угадай” не компилируется. Тут условие должно быть bool.

fn main() {
    let user = "";

    if !user.is_empty() {
        println!("Hello, {}", user);
    } else {
        println!("Hello, guest");
    }
}
  • Пустая строка? is_empty()
  • Пустой список? is_empty()
  • Может быть значение? Option
  • Может быть ошибка? Result
  • Есть несколько вариантов? match

В Python if обычно вахтер на входе:

service = "nova-api"

if service == "nova-api":
    port = 8774
elif service == "neutron-api":
    port = 9696
elif service == "keystone":
    port = 5000
else:
    port = None

print(port)

И все ок, пока кто-то через пару месяцев ниже по коду не вызовет connect(port) для barbican-api и None уезжает в функцию, которая ждала порт, а не отсутствие развернутого сервиса.

Rust требует быть честнее:

fn port_for(service: &str) -> Option<u16> {
    if service == "nova-api" {
        Some(8774)
    } else if service == "neutron-api" {
        Some(9696)
    } else if service == "keystone" {
        Some(5000)
    } else {
        None
    }
}

Он не сильно лучше Python кода с точки зрения здравого смысла, но он заставит вызывающую функцию обработать Some, None, или взять ответственность с unwrap().

match и enum Link to heading

Настоящая разница начинается там, где появляется enum:

enum Service {
    Nova,
    Neutron,
    Keystone,
}

Теперь сервис не строка, а один из заранее известных вариантов.

fn port(service: Service) -> u16 {
    match service {
        Service::Nova => 8774,
        Service::Neutron => 9696,
        Service::Keystone => 5000,
    }
}

fn main() {
    let service = Service::Nova;
    let port = port(service);

    println!("port = {}", port);
}

Функция не принимает строку. Она принимает Service.

  • Невозможно случайно передать “nvoa-api”.
  • Невозможно передать пустую строку.
  • Невозможно передать “keystone " с пробелом.
  • Невозможно передать “nova-api” в одном месте и “nova” в другом.

Да, в Python тоже есть from enum import Enum, но проблема в том, что статический анализатор может начать возмущаться, если он есть, включен, настроен, не игнорируется в CI и все участники команды не считают его личным оскорблением. Но сам Python в рантайме спокойно выполнит вызов. Строка? Строка. Функция? Функция. Какие ваши доказательства.

Теперь представим, что мы все же решили добавить новый сервис:

enum Service {
    Nova,
    Neutron,
    Keystone,
    Barbican,
}

Это вызовет ошибку в компиляции и потребует во всех функциях обработать его явно.

Формально можно использовать _ в качестве мусорного ведра

fn port(service: Service) -> u16 {
    match service {
        Service::Nova => 8774,
        Service::Neutron => 9696,
        Service::Keystone => 5000,
        _ => 8080,
    }
}
}

Но в инфраструктурном коде вы не захотите так делать. А в таком вполне себе:

fn status(code: u16) -> &'static str {
    match code {
        200 => "ok",
        400..=499 => "client error",
        500..=599 => "server error",
        _ => "unknown",
    }
}

&'static str выглядит как будто бы я упал лицом на клавиатуру, но это не так. &str это ссылка на строку, а 'static значит, что строка живёт всё время работы программы. Строковые литералы вроде "ok" и "unknown" зашиты в бинарник, поэтому функция может вернуть ссылку на них. Мы не создаём новый String, не аллоцируем память и не передаём владение. Просто возвращаем указатель на текст, который и так никуда не денется.

for Link to heading

Для итераций мы привыкли использовать циклы for и while:

services = [
    {"name": "nova-api", "port": 8774},
    {"name": "neutron-api", "port": 9696},
    {"name": "keystone", "port": 5000},
]

for service in services:
    print(service["name"], service["port"])

В Rust начинаются спецэффекты borrow checker’a:

enum ServiceKind {
    Nova,
    Neutron,
    Keystone,
}

struct Service {
    kind: ServiceKind,
    name: String,
    port: u16,
}

fn main() {
    let services = vec![
        Service {
            kind: ServiceKind::Nova,
            name: String::from("nova-api"),
            port: 8774,
        },
        Service {
            kind: ServiceKind::Neutron,
            name: String::from("neutron-api"),
            port: 9696,
        },
        Service {
            kind: ServiceKind::Keystone,
            name: String::from("keystone"),
            port: 5000,
        },
    ];

    for service in services {
        println!("{} -> {}", service.name, service.port);
    }

    println!("services count = {}", services.len());
}
36 |     println!("services count = {}", services.len());
   |                                     ^^^^^^^^ value borrowed here after move

Мы не просто проитерировались по сервисам, мы “съели” этот вектор. Элементы переезжают внутрь цикла, а после цикла services больше недоступен.

Так что итерируясь вы должны помнить про то, что либо вы забираете владение:

for service in services {
    println!("{} -> {}", service.name, service.port);
}

либо читаете по ссылке:

for service in &services {
    println!("{} -> {}", service.name, service.port);
}

либо меняете по изменяемой ссылке:

for service in &mut services {
    service.name.push_str("-internal");
}

Если нужно изменить данные, то, опять же, держим в памяти mut:

fn main() {
    let mut endpoints = vec![
        String::from("http://10.0.0.10:8774"),
        String::from("http://10.0.0.11:9696"),
        String::from("http://10.0.0.12:5000"),
    ];

    for endpoint in &mut endpoints {
        endpoint.push_str("/healthcheck");
    }

    println!("{:?}", endpoints);
}

То есть мы явно говорим: хочу поменять элементы внутри коллекции.

Если надо достать индекс, то достаем знакомый по python enumerate:

for (index, service) in services.iter().enumerate() {
    println!("{}: {}", index, service);
}

Где iter() отдаёт ссылки на элементы. Если же нужно забрать значения, есть into_iter():

for service in services.into_iter() {
    println!("{}", service);
}

А если поменять iter_mut():

fn main() {
    let mut services = vec![
        String::from("nova-api"),
        String::from("neutron-api"),
        String::from("keystone"),
    ];

    for service in services.iter_mut() {
        service.push_str("-internal");
    }

    println!("{:?}", services);
}

Это же и естественная замена list comprehension.

break и continue Link to heading

За исключением синтаксического сахара все привычно:

fn main() {
    let services = vec!["nova-api", "neutron-api", "keystone"];

    for service in &services {
        if *service == "neutron-api" {
            continue;
        }

        if *service == "keystone" {
            break;
        }

        println!("checking {}", service);
    }
}

Почему в синтаксис языка еще звездочка * просыпалась? Когда мы пишем for service in &services, мы получаем ссылку на элемент. Если вектор хранит &str, то ссылка на него будет &&str или ссылка на ссылку. Если слишком много сахара, то можно использовать pattern destructuring:

fn main() {
    let services = vec!["nova-api", "neutron-api", "keystone"];

    for &service in &services {
        if service == "neutron-api" {
            continue;
        }

        if service == "keystone" {
            break;
        }

        println!("checking {}", service);
    }
}

Выходить из вложенных циклов удобнее через метки:

struct Endpoint {
    interface: String,
    url: String,
}

struct Service {
    name: String,
    endpoints: Vec<Endpoint>,
}

fn main() {
    let services = vec![
        Service {
            name: String::from("nova-api"),
            endpoints: vec![
                Endpoint {
                    interface: String::from("public"),
                    url: String::from("https://public.example.com:8774"),
                },
                Endpoint {
                    interface: String::from("internal"),
                    url: String::from("http://10.0.0.10:8774"),
                },
            ],
        },
        Service {
            name: String::from("keystone"),
            endpoints: vec![
                Endpoint {
                    interface: String::from("public"),
                    url: String::from("https://public.example.com:5000"),
                },
            ],
        },
    ];

    'search: for service in &services {
        for endpoint in &service.endpoints {
            if endpoint.interface == "internal" {
                println!("found internal endpoint: {}", endpoint.url);
                break 'search;
            }
        }
    }
}

Метка 'search позволяет выйти не только из внутреннего цикла, а сразу из внешнего без found = True и отдельного условия if found: break.

while Link to heading

While в Rust скучный и это комплимент:

fn main() {
    let mut retries = 3;

    while retries > 0 {
        println!("trying to reach API endpoint...");
        retries -= 1;
    }

    println!("giving up");
}

while True, тоже, в общем-то:

loop {
    println!("работаем, братья");
}

Для выхода из loop можно использовать классический подход Python:

fn main() {
    let mut attempts = 0;

    let status = loop {
        attempts += 1;

        println!("checking endpoint, attempt {}", attempts);

        if attempts == 3 {
            break "endpoint is alive";
        }

        if attempts > 5 {
            break "endpoint is dead";
        }
    };

    println!("{}", status);
}

any() / all() Link to heading

Тут, в общем, тоже без приключений:

has_internal = any(
    endpoint["interface"] == "internal"
    for endpoint in endpoints
)

Элегантно превращается в:

let has_internal = services
    .iter()
    .flat_map(|service| service.endpoints.iter())
    .any(|endpoint| endpoint.interface == "internal");
let all_have_url = services
    .iter()
    .flat_map(|service| service.endpoints.iter())
    .all(|endpoint| !endpoint.url.is_empty());