Rust 枚举、匹配和选项 API

1,137 阅读8分钟

Rust 枚举、匹配和选项 API

如果您在过去几年中一直活跃于编程社区,那么您无疑听说过Rust。它的技术基础和充满活力的社区已证明自己是快速语言增长的良好基准。

但是 Rust 做了什么让社区产生了如此积极的反应?Rust 不仅提供了大量的内存安全性(这在过去的低级语言中很少见),而且还包括使开发变得更好的强大功能。

突出 Rust 功能的众多特性之一是它对枚举和匹配的处理。

枚举

像许多具有严格类型的语言一样,Rust有一个 enum 特性。声明一个枚举很简单,从pub enum值开始并命名。

pub enum CodeLang {
    Rust,
    JavaScript,
    Swift,
    Kotlin,
    // ...
}

要创建具有该枚举类型的变量,您可以使用枚举的名称和值:

fn main() {
   let lang = CodeLang::Rust;
}

同样,您可以enum在函数参数之类的地方使用作为类型。假设您想检测 CoderPad 支持哪个版本的编程语言。我们将从硬编码 Rust 版本开始:

fn get_version(_lang: CodeLang) -> &'static str {
   return "1.46";
}

虽然此代码有效,但它的功能不是很强大。如果传入“CodeLang::JavaScript”,则版本号不正确。让我们来看看如何在下一节中解决这个问题。

匹配

虽然您可以使用if语句来检测传入的枚举,如下所示:

fn get_version(lang: CodeLang) -> &'static str {
    if let CodeLang::Rust = lang {
        return "1.46";
    }
    
    if let CodeLang::JavaScript = lang {
        return "2021";
    }
    
    return ""
}

fn main() {
    let lang = CodeLang::Rust;

    let ver = get_version(lang);

    println!("Version {}", ver);
}

当处理枚举中的一两个以上值时,这很容易变得笨拙。这就是 Rust 的match操作符发挥作用的地方。让我们将变量与枚举中的所有现有值进行匹配:

fn get_version(lang: CodeLang) -> &'static str {
   match lang {
       CodeLang::Rust => "1.46",
       CodeLang::JavaScript => "2021",
       CodeLang::Swift => "5.3",
       CodeLang::Python => "3.8"
   }
}

如果您熟悉具有类似于“ switch/case ”功能的编程语言,那么这个示例是该功能的近似。但是,您很快就会看到,matchRust 比大多数 switch/case 实现要强大得多。

模式匹配

虽然大多数实现switch/case只允许简单的原语匹配,例如字符串或数字,但 Rustmatch允许您对匹配的内容和方式进行更精细的控制。例如,您可以使用_标识符匹配任何不匹配的内容:

fn get_version(lang: CodeLang) -> &'static str {
   match lang {
       CodeLang::Rust => "1.46",
       _ => "Unknown version"
   }
}

您还可以一次匹配多个值。在这个例子中,我们一次检查不止一种编程语言的版本。

fn get_version<'a>(lang: CodeLang, other_lang: CodeLang) -> (&'a str, &'a str) {
   match (lang, other_lang) {
       (CodeLang::Rust, CodeLang::Python) => ("1.46", "3.8"),
       _ => ("Unknown", "Unknown")
   }
}

这显示了match. 但是,您可以使用枚举做更多事情。

价值存储

不仅是枚举值本身,而且您还可以将值存储在枚举中以供以后访问。

例如,CoderPad 支持两个不同版本的 Python。但是,我们可以使用一个值并将主要版本存储在其中,而不是创建 aCodeLang::PythonCoderLang::Python2enum 值。

pub enum CodeLang {
   Rust,
   JavaScript,
   Swift,
   Python(u8),
   // ...
}

fn main() {
   let python2 = CodeLang::Python(2);

   let pythonVer = get_version(python2);
}

我们能够if let从之前扩展我们的表达式以访问其中的值:

if let CodeLang::Python(ver) = python2 {
    println!("Python version is {}", ver);
}

然而,就像以前一样,我们能够利用match来解压枚举中的值:

fn get_version(lang: CodeLang) -> &'static str {
   match lang {
       CodeLang::Rust => "1.46",
       CodeLang::JavaScript => "2021",
       CodeLang::Python(ver) => {
           if ver == 3 { "3.8" } else { "2.7" }
       },
        _ => "Unknown"
   }
}

然而,并非所有枚举都需要手动设置!Rust 有一些内置于语言中的枚举,随时可以使用。

选项枚举

虽然我们目前将字符串”Unknown”作为一个版本返回,但这并不理想。也就是说,我们必须进行字符串比较以检查我们是否返回了已知版本,而不是使用专用于缺少值的值。

这就是 Rust 的Option枚举发挥作用的地方。Option<T>描述了一种具有Some(data)None可说的数据类型。

例如,我们可以将上面的函数重写为:

fn get_version<'a>(lang: CodeLang) -> Option<&'a str> {
   match lang {
       CodeLang::Rust => Some("1.46"),
       CodeLang::JavaScript => Some("2021"),
       CodeLang::Python(ver) => {
           if ver == 3 { Some("3.8") } else { Some("2.7") }
       },
        _ => None
   }
}

通过这样做,我们可以使我们的逻辑更具代表性并检查一个值是否是 None

fn main() {
    let swift_version = get_version(CodeLang::Swift);

    if let None = swift_version {
        println!("We could not find a valid version of your tool");
        return;
    }
}

最后,我们当然可以使用match从 an 迁移if来检查何时设置了值:

fn main() {
    let code_version = get_version(CodeLang::Rust);

    match code_version {
        Some(val) => {
            println!("Your version is {}", val);
        },
        None => {
            println!("We could not find a valid version of your tool");
            return;
        }
    }
}

运营商

虽然上述代码按预期运行,但如果我们添加更多条件逻辑,我们可能会发现自己想要进行抽象。让我们看看 Rust 为我们提供的一些抽象

地图运算符

如果我们想转换rust_version为字符串,但想处理存在的边缘情况怎么办None

你可能会这样写:

fn main() {
    let rust_version = get_version(CodeLang::Rust);

    let version_str = match rust_version {
        Some(val) => {
            Some(format!("Your version is {}", val))
        },
        None => None
    };
    
    if let Some(val) = version_str {
        println!("{}", val);
        return;
    }
}

这种match获取Some并将其映射到一个新值并让Nones 解析为None仍然被烘焙到 Option 枚举中的方法称为.map

fn main() {
    let rust_version = get_version(CodeLang::Rust);

    let version_str = rust_version.map(|val| {
      format!("Your version is {}", val)
    });
    
    if let Some(val) = version_str {
        println!("{}", val);
        return;
    }
}

实现与.map我们之前所做的工作有多接近?我们来看看Rust 的源代码实现.map

pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
   match self {
       Some(x) => Some(f(x)),
       None => None,
   }
}

如您所见,我们非常相似地匹配我们的实现,匹配Some到另一个SomeNone另一个None

然后操作员

虽然在大多数情况下将.map函数返回值自动包装到 aSome中很有用,但有时您可能希望有条件地在map

假设我们只想要包含一个点的版本号(表示有一个次要版本)。我们可以这样做:

fn main() {
    let rust_version = get_version(CodeLang::JavaScript);

    let version_str = match rust_version {
        Some(val) => {
            if val.contains(".") {
                Some(format!("Your version is {}", val))
            } else {
                None
            }
        },
        None => None
    };
    
    if let Some(val) = version_str {
        println!("{}", val);
        return;
    }
}

我们可以使用 Rust 的and_then运算符重写它:

fn main() {
    let rust_version = get_version(CodeLang::JavaScript);

    let version_str = rust_version.and_then(|val| {
        if val.contains(".") {
            Some(format!("Your version is {}", val))
        } else {
            None
        }
    });
    
    if let Some(val) = version_str {
        println!("{}", val);
        return;
    }
}

如果我们看一下操作员锈病的源代码,我们可以看到相似的.map实现,只是没有包装fnSome

pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> {
        match self {
            Some(x) => f(x),
            None => None,
        }
    }

把它放在一起

现在我们已经熟悉了 Option 枚举、运算符和模式匹配,让我们把它们放在一起!

让我们从get_version用于几个示例的相同函数基线开始:

use regex::Regex;

pub enum CodeLang {
   Rust,
   JavaScript,
   Swift,
   Python(u8),
   // ...
}

fn get_version<'a>(lang: CodeLang) -> Option<&'a str> {
   match lang {
       CodeLang::Rust => Some("1.46"),
       CodeLang::JavaScript => Some("2021"),
       CodeLang::Python(ver) => {
           if ver == 3 { Some("3.8") } else { Some("2.7") }
       },
        _ => None
   }
}

fn main() {
    let lang = CodeLang::JavaScript;

    let lang_version = get_version(lang);
}

鉴于此基线,让我们构建一个 semver 检查器。给定一种编码语言,告诉我们该语言的主要和次要版本是什么。

例如,Rust (1.46) 会返回“ Major: 1. Minor: 46 ”,而 JavaScript (2021) 会返回**“Major: 2021. Minor: 0** ”

我们将使用解析版本字符串中的任何点的正则表达式来执行此检查。

(\d+)(?:\.(\d+))?

此正则表达式将匹配第一个捕获组作为第一个期间之前的任何内容,然后可选地提供第二个捕获(如果有一个期间),匹配该期间之后的任何内容。让我们在main函数中添加正则表达式和捕获:

let version_regex = Regex::new(r"(\d+)(?:\.(\d+))?").unwrap();

let version_matches = lang_version.and_then(|version_str| {
    return version_regex.captures(version_str);
});

在上面的代码示例中,我们使用and_then以扁平captures化为单层Option枚举 -将 Option 视为lang_version本身并captures返回一个 Option。

虽然.captures听起来它应该返回一个捕获字符串的数组,但实际上它返回一个具有各种方法和属性的结构。要获取每个值的字符串,我们将使用version_matches.map获取这两个捕获组字符串:

let major_minor_captures = version_matches
        .map(|caps| {
            (
                caps.get(1).map(|m| m.as_str()),
                caps.get(2).map(|m| m.as_str()),
            )
        });

虽然我们希望捕获组 1 始终提供一个值(给定我们的输入),但如果没有句点(例如 JavaScript 的版本号“2021”),我们会看到捕获组 2 中返回“None”。因此,在某些情况下caps.get(2)可能是None。因此,我们希望确保0在 的位置获得 aNone并将其转换Some<&str>, Option<&str>Some<&str, &str>。为此,我们将使用and_then和 a match

let major_minor = major_minor_captures
    .and_then(|(first_opt, second_opt)| {
        match (first_opt, second_opt) {
            (Some(major), Some(minor)) => Some((major, minor)),
            (Some(major), None) => Some((major, "0")),
            _ => None,
        }
    });

最后,我们可以使用 anif let来解构值并打印主要和次要版本:

if let Some((first, second)) = major_minor {
    println!("Major: {}. Minor: {}", first, second);
}

项目的最终版本应如下所示:

use regex::Regex;

pub enum CodeLang {
   Rust,
   JavaScript,
   Swift,
   Python(u8),
   // ...
}

fn get_version<'a>(lang: CodeLang) -> Option<&'a str> {
   match lang {
       CodeLang::Rust => Some("1.46"),
       CodeLang::JavaScript => Some("2021"),
       CodeLang::Python(ver) => {
           if ver == 3 { Some("3.8") } else { Some("2.7") }
       },
        _ => None
   }
}

fn main() {
    let lang = CodeLang::JavaScript;

    let lang_version = get_version(lang);

    let version_regex = Regex::new(r"(\d+)(?:\.(\d+))?").unwrap();

    let version_matches = lang_version.and_then(|version_str| {
        return version_regex.captures(version_str);
    });
    
    let major_minor_captures = version_matches
        .map(|caps| {
            (
                caps.get(1).map(|m| m.as_str()),
                caps.get(2).map(|m| m.as_str()),
            )
        });


    let major_minor = major_minor_captures
        .and_then(|(first_opt, second_opt)| {
            match (first_opt, second_opt) {
                (Some(major), Some(minor)) => Some((major, minor)),
                (Some(major), None) => Some((major, "0")),
                _ => None,
            }
        });


    if let Some((first, second)) = major_minor {
        println!("Major: {}. Minor: {}", first, second);
    }
}

结论与挑战

所有这些特性在 Rust 应用程序中都经常使用:枚举、匹配、选项运算符。我们希望您可以在学习 Rust 的过程中利用这些功能并在您的应用程序中使用它们。

让我们以挑战结束。如果您在此过程中遇到任何问题或对本文有任何意见/问题,您可以加入我们的公共聊天社区,在那里我们讨论一般编码主题以及采访

假设我们跟踪了软件的“补丁”版本。我们希望扩展我们的代码逻辑以支持检查“5.1.2”并返回“2”作为“补丁”版本。给定修改后的正则表达式以支持三个可选的捕获组:

(\d+)(?:\.(\d+))?(?:\.(\d+))?

您如何修改下面的代码以支持正确列出的匹配版本?

当您能够输出以下内容时,您就会知道代码正在运行:

Major: 2021. Minor: 0, Patch: 0
Major: 1. Minor: 46, Patch: 0
Major: 5. Minor: 1, Patch: 2