generics - una - ¿Por qué los operadores de Rust tienen el tipo de variable de salida?
tipos de variables en programacion (2)
Esta pregunta ya tiene una respuesta aquí:
He estado leyendo el libro de Rust en línea y he llegado a 4.1, Operadores y sobrecarga . Noté que std::ops::Add
define como fn add(self, rhs: RHS) -> Self::Output;
con un type Output
definida por separado en el rasgo.
Entiendo lo que está sucediendo aquí: la función de add
acepta el lado izquierdo como self
, y el lado derecho como el parámetro genérico tipo RHS
.
Quiero saber por qué el tipo de salida se define con el alias de Output
, en lugar de ser otro genérico (por ejemplo, Add<RHS, Output>
)? ¿Es solo una convención, o hay una razón específica para ello?
Se trata de quién llega a especificar el tipo de salida. En general, cuando multiplicas dos tipos, digamos f64
por f64
, está claro el tipo de resultado que debería ser: f64
. No tendría sentido que la persona que llama solicite un tipo de resultado de String
, por lo que no tiene sentido como entrada.
Me puedo imaginar algunos casos en los que es posible que desee la opción de salida diferente, pero son un nicho poco (por ejemplo, u32
* u32
devolver el resultado u64
completo?); Además, si hubiera más de una opción, ¿cómo se especifica cuál quiere? Si bien podrías escribir Add<u32, u64>::add(a, b)
, las expresiones simples como a*b*c
serían ambiguas y harían la inferencia del tipo mucho más difícil.
Mientras que las funciones operan en variables, puede pensar en rasgos como funciones que operan en tipos. Cuando se piensa de esta manera, los parámetros tipo sirven como entradas para la función y los tipos asociados sirven como salidas.
Como Output
es un tipo asociado, para dos tipos A
y B
que deseamos impl Add
, estamos limitados a elegir un solo tipo de Output
. Si Output
fuera un parámetro de tipo, podríamos impl Add
para A
y B
en una miríada de formas.
Por ejemplo, definamos un rasgo Mul
donde Output
es un parámetro:
trait Mul<RHS, Output> {
fn mul(self, rhs: RHS) -> Output;
}
Ahora definamos un tipo Complex
:
#[derive(Debug, Clone, Copy)]
struct Complex {
x: f64,
y: f64,
}
impl Complex {
fn new(x: f64, y: f64) -> Complex {
Complex { x: x, y: y }
}
}
Queremos poder multiplicarlo por f64
:
impl Mul<f64, Complex> for Complex {
fn mul(self, rhs: f64) -> Complex {
Complex::new(self.x * rhs, self.y * rhs)
}
}
Todo esto funciona bien Sin embargo, podríamos llegar a una segunda implementación:
impl Mul<f64, f64> for Complex {
fn mul(self, rhs: f64) -> f64 {
self.x * rhs
}
}
Cuando multiplicamos un Complex
por un f64
ahora, es ambiguo a qué implementación debe llamarse, y la persona que llama necesita información de tipo adicional. Al hacer que Output
un tipo asociado, esto no se permite. El siguiente código arroja un error de compilación para implementaciones conflictivas:
impl std::ops::Mul<f64> for Complex {
type Output = Complex;
fn mul(self, rhs: f64) -> Complex {
Complex::new(self.x * rhs, self.y * rhs)
}
}
impl std::ops::Mul<f64> for Complex {
type Output = f64;
fn mul(self, rhs: f64) -> Complex {
self.x * rhs
}
}