Pull to refresh

Rust: пробуем перегрузку функций

Reading time 5 min
Views 7.1K
Original author: Casper

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


После нескольких попыток задача была успешно решена. Как — под катом.


Игры с типажами не работают.


trait FooA { fn foo(_: i32); }
trait FooB { fn foo(_: &str); }

struct Foo;
impl FooA for Foo { fn foo(_: i32) { println!("FooA"); } }
impl FooB for Foo { fn foo(_: &str) { println!("FooB"); } }

Попробуем вызвать функцию с аргументом типа &str.


fn main() {
    Foo::foo("hello");
}

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


Наоборот, данный пример требует однозначного указания вызываемой функции:


fn main() {
    <Foo as FooB>::foo("hello");
}

Код


Однако это сводит на нет все преимущества, предоставляемые перегрузкой. В конце этой заметки я покажу, что на Rust реализуема традиционная перегрузка функций — посредством использования типажей и обобщенного программирования — generic'ов.


Статический полиморфизм


Для разрешения методу принятия различных типов аргументов Rust использует статический полиморфизм с generic'ами.


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


Они могут быть простыми, например, AsRef, чтобы позволить вашему API принимать больше вариантов аргументов:


fn print_bytes<T: AsRef<[u8]>>(bytes: T) {
    println!("{:?}", bytes.as_ref());
}

В вызывающем коде это похоже на перегрузку:


fn main() {
    print_bytes("hello world");
    print_bytes(&[12, 42, 39, 15, 91]);
}

Код


Вероятно, лучшим примером этого является принимающий несколько типов аргументов
типаж ToString:


fn print_str<T: ToString>(value: T) {
    let s = value.to_string();
    println!("{}", s);
}

fn main() {
    print_str(42);
    print_str(3.141593);
    print_str("hello");
    print_str(true);
    print_str('');
}

Код


Эта разновидность перегрузки делает ваш API более удобным для использования вашими пользователями. Им не нужно будет обременять себя переводом аргументов в нужный тип, API этого не требует. В итоге получается API, с которым приятно работать.


Данный подход имеет преимущества по сравнения с привычной перегрузкой, ибо реализация типами (пользователя) типажей позволяет вашему API принимать разные пользовательские типы.


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


Интермедия: избыточный generic-код


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


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


mod stats {
    pub fn stddev<T: ?Sized + AsRef<[f64]>>(values: &T) -> f64 {
        stddev_impl(values.as_ref())
    }
    fn stddev_impl(values: &[f64]) -> f64 {
        let len = values.len() as f64;
        let sum: f64 = values.iter().cloned().sum();
        let mean = sum / len;
        let var = values.iter().fold(0f64, |acc, &x| acc + (x - mean) * (x - mean)) / len;
        var.sqrt()
    }
}
pub use stats::stddev;

Несмотря на то, что функция вызвана с двумя разными типами (&[f64] и &Vec<f64>) основная логика функции реализована (и скомпилирована) только один раз, что предотвращает чрезмерное раздувание бинарников.


fn main() {
    let a = stddev(&[600.0, 470.0, 170.0, 430.0, 300.0]);
    let b = stddev(&vec![600.0, 470.0, 170.0, 430.0, 300.0]);

    assert_eq!(a, b);
}

Код


Проверяем границы


Не всякая перегрузка подпадает под эту категорию простых преобразований аргументов. Иногда вам действительно требуется разная логика для обработки разных наборов принимаемых аргументов. Для данных случаев вы можете определить свой типаж для реализации программной логики вашей функции:


pub struct Foo(bool);

pub trait CustomFoo {
    fn custom_foo(self, this: &Foo);
}

Это делает типаж очень неуклюжим, ибо self и аргументы поменяны местами:


impl CustomFoo for i32 {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) i32: {}", this.0, self);
    }
}
impl CustomFoo for char {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) char: {}", this.0, self);
    }
}
impl<'a, S: AsRef<str> + ?sized> CustomFoo for &'a S {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) str: {}", this.0, self.as_ref());
    }
}

Типаж не может быть скрыт как деталь реализации. Если вы решите сделать типаж приватным, компилятор выдаст следующее: private trait in public interface.


Давайте сделаем обертку над типажом:


pub struct Foo(bool);

impl Foo {
    pub fn foo<T: CustomFoo>(&self, arg: T) {
        arg.custom_foo(self);
    }
}

fn main() {
    Foo(false).foo(13);
    Foo(true).foo(''));
    Foo(true).foo("baz");
}

Код


Применение данного приема можно найти в стандартной библиотеке в типаже Pattern, который используется разными функциями, которые ищут или тем или иным образом сопоставляют строки, например, str::find.


В отличие от вас, стандартная библиотека имеет возможность скрывать данные типажи, в то же время давая им возможность использоваться в публичных интерфейсах посредством атрибута #[unstable].


Попасть одним выстрелом в двух зайцев


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


Создайте типаж для функции, сигнатуру которой вы желаете перегрузить с обобщенными параметрами на месте "перегружаемых" параметров.


trait OverloadedFoo<T, U> {
    fn overloaded_foo(&self, tee: T, yu: U);
}

Ограничения на типажи в Rust являются очень мощным инструментом.


При реализации метода, просто ограничьте Self, чтобы он реализовывал типаж и обобщенные параметры, в которых нуждается ваш типаж. Для Rust это достаточно:


struct Foo;
impl Foo {
    fn foo<T, U>(&self, tee: T, yu: U) where Self: OverloadedFoo<T, U> {
        self.overloaded_foo(tee, yu)
    }
}

После этого реализуйте типаж для всех типов, для которых вы хотите предоставить перегрузку:


impl OverloadedFoo<i32, f32> for Foo {
    fn overloaded_foo(&self, tee: i32, yu: f32) {
        println!("foo<i32, f32>(tee: {}, yu: {})", tee, yu);
    }
}

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


impl<'a, S: AsRef<str> + ?Sized> OverloadedFoo<&'a S, char> for Foo {
    fn overloaded_foo(&self, tee: &'a S, yu: char) {
        println!("foo<&str, char>(tee: {}, yu: {})", tee.as_ref(), yu);
    }
}

Это все!


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


fn main() {
    Foo.foo(42, 3.14159);
    Foo.foo("hello", '');
    // Foo.foo('', 13); // ограничения типажа не соблюдены
}

Код


Вывод


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

Only registered users can participate in poll. Log in, please.
Как вы относитесь к перегрузке функций в Rust?
51.16% Следует добавить 44
17.44% Не нужно, ибо усложнит язык 15
31.4% Не нужно, ибо хватает того, что есть 27
86 users voted. 34 users abstained.
Tags:
Hubs:
+11
Comments 4
Comments Comments 4

Articles