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

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

use num_traits::Num;

fn main() {
    println!("{}", sum_two_numbers(5, 6));
}

fn sum_two_numbers<T: Num, U: Num>(num1: T, num2: U) -> U{
    num1 + num2
}

Вышеупомянутое не компилируется. Я получаю следующую ошибку:

error[E0308]: mismatched types
  --> src\main.rs:13:12
   |
12 | fn sum_two_numbers<T: Num, U: Num>(num1: T, num2: U) -> U{
   |                    -       - found type parameter
   |                    |
   |                    expected type parameter
13 |     num1 + num2
   |     ----   ^^^^ expected type parameter `T`, found type parameter `U`
   |     |
   |     expected because this is `T`
   |
   = note: expected type parameter `T`
              found type parameter `U`
   = note: a type parameter was expected, but a different one was found; you might be missing a type parameter 
or trait bound
   = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters 

error[E0308]: mismatched types
  --> src\main.rs:13:5
   |
12 | fn sum_two_numbers<T: Num, U: Num>(num1: T, num2: U) -> U{
   |                    -       - expected type parameter    - expected `U` because of return type
   |                    |
   |                    found type parameter
13 |     num1 + num2
   |     ^^^^^^^^^^^ expected type parameter `U`, found type parameter `T`
   |
   = note: expected type parameter `U`
              found type parameter `T`
   = note: a type parameter was expected, but a different one was found; you might be missing a type parameter 
or trait bound
   = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters 

For more information about this error, try `rustc --explain E0308`.
error: could not compile `generics-for-stackoverflow-question` (bin "generics-for-stackoverflow-question") due 
to 2 previous errors

Я знаю, что типы i32 можно преобразовать в f64 следующим образом:

5 as f64;

Я надеялся, что компилятор сможет понять, что я пытаюсь сложить два разных типа чисел, и поможет мне их преобразовать. Очевидно, что это не работает.

Обратите внимание, что черта Num взята из внешнего ящика, который я добавил в свой проект (ящик — num_traits).

Ранее я написал функцию таким образом, чтобы она компилировалась, как показано ниже:

use num_traits::Num;

fn main() {
    println!("{}", sum_two_numbers(5, 6));
}

fn sum_two_numbers<T: Num>(num1: T, num2: T) -> T{
    num1 + num2
}

Это позаботится о добавлении целого числа к целому числу или числа с плавающей запятой к числу с плавающей запятой, но не добавления числа с плавающей запятой к целому числу или наоборот.


1
87
1

Ответ:

Решено

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

Так как же нам заставить компилятор позволить вам выполнить это преобразование? Предположим, мы сначала хотим преобразовать тип первого числа (T) в тип второго числа (U). Прежде чем выполнять сложение, нам нужно потребовать, чтобы тип U поддерживал автоматическое преобразование из типа T. Мы делаем это, требуя от U реализовать не только Num, но и From<T>. После этого мы получим доступ к этим дополнительным функциям и сможем преобразовать num1 в U. Тогда дополнение будет работать нормально.

fn sum_two_numbers<T: Num, U: Num + From<T>>(num1: T, num2: U) -> U{
    U::from(num1) + num2
}

Обратите внимание, что обычно для числовых типов определяется только преобразование, не связанное с риском потери точности, поэтому вы можете автоматически конвертировать, например, из от u8 до u16, но не наоборот. Итак, это отлично работает:

// 5u8 is first converted to u16, and then added to 6u16
println!("{}", sum_two_numbers(5u8, 6u16));

Но это не компилируется:

println!("{}", sum_two_numbers(5u16, 6u8));
  |                    ---------------       ^^^ the trait `From<u16>` is not implemented for `u8`
  |                    |
  |                    required by a bound introduced by this call

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

fn sum_two_numbers2<T: Num, U: Num + TryFrom<T>>(num1: T, num2: U) -> Result<U, U::Error> {
    Ok(U::try_from(num1)? + num2)
}

Оператор вопросительного знака заставляет функцию возвращать ошибку в случае сбоя преобразования. В противном случае результат сложения оборачивается Ok() результатом, что указывает на успех.

Предыдущие примеры теперь компилируются и работают нормально (здесь я также использую оператор вопросительного знака, чтобы функция main возвращала ошибку в случае сбоя преобразования):

fn main() -> Result<(), Box<dyn Error>> {
    println!("{}", sum_two_numbers2(5u16, 6u8)?);
    println!("{}", sum_two_numbers2(5u8, 6u16)?);
    Ok(())
}
// prints "11" twice

В случае, если число слишком велико, выручит:

fn main() -> Result<(), Box<dyn Error>> {
    println!("{}", sum_two_numbers2(1000u16, 6u8)?);
    Ok(())
}
// prints: Error: TryFromIntError(())

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

println!("{}", sum_two_numbers(1000u16, 6.3f32));
println!("{}", sum_two_numbers2(1000u16, 6.3f32)?);
// prints: 1006.3 twice

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

Теперь, добавляя обработку ошибок, у нас может возникнуть соблазн также поддержать проверку на переполнение и возврат ошибки в этом случае (например, вы можете сделать 200u8.checked_add(200u8) и получить None, чтобы указать на переполнение), но, увы, крейт num_traits, похоже, не поддерживает это.