在 Java、C#、C++、Kotlin 等语言中,我们可以定义多个同名函数,但它们的参数不同:
void connect(int ip, int port) { … }
void connect(int ip) { … }
void connect(string address) { … }
这种语言特性被称为“函数重载(编译时多态)”。就我个人而言,在这些语言中编程多年后,我已经习惯了这种特性,并期望语言能够支持它。但如果你在 Rust 中尝试这样做:
fn add(a: i32, b: i32) {}
fn add(a: u32, b: u32) {}
你会得到一个错误:
但多态并不仅仅局限于函数重载。你还可以做更多事情并称之为多态。让我们来讨论一下。
1. 枚举(健壮的语法)
enum ServerAddress {
Ip(u32), // IPv4 是 32 位(4 字节)
IpAndPort(u32, u16),
Address(String),
}
fn connect(address: ServerAddress) {
match address {
ServerAddress::Ip(ip) => {},
ServerAddress::IpAndPort(ip, port) => {},
ServerAddress::Address(address) => {},
}
}
你可以这样调用:
connect(ServerAddress::Ip(1));
connect(ServerAddress::IpAndPort(1, 80));
connect(ServerAddress::Address("192.168.1.1:80".to_string()));
使用枚举有时是动态的,有时是静态的。如果编译器能够在编译时推断出枚举的变体,它会跳过运行时的类型检查,从而提高速度。但如果变体在编译时未知,它会在运行时进行类型检查,这会带来性能开销。
你可能会觉得这种语法过于复杂和冗长,是的,确实如此!但通过阅读函数调用,你能获得更多信息:
// Java
connect("192.168.1.1:80");
// 这意味着有一个名为 'connect' 的函数,接受一个字符串参数。
// Rust
connect(ServerAddress::Address("192.168.1.1:80".to_string()));
// 这意味着你可以用一个字符串参数调用 connect 函数,
// 但还有更多选项,你可以在 ServerAddress 枚举中找到其他选项。
虽然冗长,但阅读时能获得更多信息。
不过!!还有更多方法,让我继续“烹饪” 🧑🍳。
2. Trait(编译时且语法简洁)👽
不同的函数重载只是为调用者提供了多种选择,而 connect 函数实际上只需要底层的 ip: u32 和 port: u16。因此,我们可以为此定义一个结构体。
struct NetworkAddress {
ip: u32,
port: u16,
}
fn connect(address: NetworkAddress) {}
有一个通用的 trait 叫 Into,用于类型转换。我们可以为 NetworkAddress 实现 Into,以便将其他类型转换为 NetworkAddress。
但建议实现 From 而不是 Into,这样会自动为我们实现 Into,从而可以调用 into() 函数。
例如,为 u32 实现 From:
// 仅 IP,默认端口 80
impl From<u32> for NetworkAddress {
fn from(value: u32) -> Self {
NetworkAddress { ip: value, port: 80 } // 80 作为默认端口
}
}
就是这样!现在我们可以用 u32 调用 connect:
// 这会通过调用你的转换函数将 u32 转换为 NetworkAddress。
connect(1.into());
现在我们可以为任何需要的类型实现这个 trait:
impl From<&str> for NetworkAddress {
fn from(value: &str) -> Self {
todo!(); // 解析地址
}
}
调用者可以这样:
connect("192.168.1.1:80".into());
那么,对于 (ip: u32, port: u16) 这种参数数量不同的情况呢?在这种情况下,你可以使用元组:
impl From<(u32, u16)> for NetworkAddress {
fn from(value: (u32, u16)) -> Self {
NetworkAddress { ip: value.0, port: value.1 }
}
}
调用者会有些奇怪:
connect((1, 80).into()); // (1,80) 是一个 (u32, u16) 元组
// 语法有点丑陋,我知道。
当然,你也可以直接传递 NetworkAddress:
connect(NetworkAddress { ip: 1, port: 80 });
这种方法的好处是它非常可扩展。调用者可以为任何需要的类型实现 Into,而这在枚举技术中是不可能做到的,除非修改枚举本身。
如果需要更简洁的语法,你可以将 into() 调用移到 connect 函数内部:
// 这意味着该函数的输入参数应该实现 Into<NetworkAddress> trait
// 这也是静态分发
fn connect(address: impl Into<NetworkAddress>) {
let address: NetworkAddress = address.into();
}
现在你可以从调用者中移除 .into():
connect("192.168.1.1:80"); // &str
connect(NetworkAddress { ip: 1, port: 80 }); // NetworkAddress
connect((1, 80)); // 元组 (u32, u16)
“好的、坏的和丑的。”
注意:使用 impl 时,它是静态分发,意味着类型检查在编译时进行,但你也可以使用 dyn 关键字以动态分发的方式实现,这在运行时会慢一些,但更灵活。
缺点是,如果你想传递多个参数给函数,你需要使用额外的“( )”,这很丑陋(我们会解决这个问题,让我继续“烹饪” 🧑🍳!好吗?)
3. 不同的函数名(简单 🤷♂️)
在讨论了 trait 和枚举之后,这看起来可能有些愚蠢,但有时这样做是可以的。
fn connect_with_ip(ip: u32) { … }
fn connect_with_ipv6(ip: u128) { … }
fn connect_with_ip_port(ip: u32, port: u16) { … }
fn connect_with_ipv6_port(ip: u128, port: u16) { … }
fn connect_with_address(addr: String) { … }
很多人更喜欢这种方法(我也是),有趣的是,Rust 标准库中大量使用了这种技术,为什么不呢?🤷♂️:
Vec::new();
Vec::with_capacity(usize);
Vec::from_raw_parts(ptr: *mut T, length: usize, capacity: usize);
// 所有这些函数都是 Vec 的工厂函数,但名称不同。
这种技术也常与宏结合使用。
4. 宏(10x 开发者 🥷)
假设我们有两个参数数量不同的函数:
fn connect_with_ip(ip: u32) {} // 一个参数
fn connect_with_ip_and_port(ip: u32, port: u16) {} // 两个参数
我们可以简单地编写一个宏:
macro_rules! connect {
($param:expr) => {
connect_with_ip($param);
};
($param1:expr, $param2:expr) => {
connect_with_ip_and_port($param1, $param2);
};
}
这个宏会在编译时为你生成代码,并根据你传递的参数数量调用相应的函数。
现在我们可以调用宏:
connect!(12); // 生成:connect_with_ip(12);
connect!(12, 80); // 生成:connect_with_ip_and_port(12, 80);
// 没有丑陋的 () 元组语法
5. 宏 + Trait(20x 开发者 🧙♂️)
最后的终极解决方案!
macro_rules! connect {
($param:expr) => {
connect_with_ip($param);
};
($param1:expr, $param2:expr) => {
connect_with_ip_and_port($param1, $param2);
};
}
struct IP(u32);
impl From<u32> for IP {
fn from(value: u32) -> Self {
IP(value)
}
}
impl From<&str> for IP {
fn from(value: &str) -> Self {
todo!() // 解析
}
}
fn connect_with_ip(ip: impl Into<IP>) {
let ip: IP = ip.into();
}
fn connect_with_ip_and_port(ip: impl Into<IP>, port: u16) {
let ip: IP = ip.into();
}
fn main() {
connect!("192.168.1.1", 80);
connect!("192.168.1.1");
connect!(1234, 80);
connect!(1234);
}
好处:
没有“( )”和元组语法!(没有丑陋的语法) 支持不同数量的参数 静态分发 没有 into() 和 try_into() 调用 当你编写宏并告诉你的技术宅朋友时,你会感觉像个忍者!他们会觉得这像外星语言一样(主要好处)。 总结 如你所见,你可以将不同的技术混合使用:
枚举 + Trait 宏 + Trait(已解释) 枚举 + 宏 + Trait 枚举 + 宏? 通过这些技术,你可以最大化函数的灵活性。