内容简介:This is the second part of Rust mini-series. Those of you who haven’t seen the first part yet are encouraged totake a look at it.Before we begin, let me point out that the aim of the series is to give a gentle introduction to Rust programming language supp
This is the second part of Rust mini-series. Those of you who haven’t seen the first part yet are encouraged totake a look at it.
Before we begin, let me point out that the aim of the series is to give a gentle introduction to Rust programming language supported by some practical examples. Please note that this is not by any means a systematic introduction to Rust, but rather a coffee break story for those who heard a little bit about Rust and want to get better understanding of the features of the language.
For actual learners struggling hard with Rust these articles can be also of interest, mainly because of a possibly different perspective on selected topics or bringing up some details that may have escaped your attention while reading more comprehensive sources.
In this part we will discuss enums , pattern matching and, what is very characteristic of Rust, Options .
Enums
Rust enums , as in other programming languages, are data types consisting of a set of named variants. But what makes Rust enum truly powerful is that all enum variants can have data associated with it. Syntax for defining associated data is analogous to struct definition – you can use tuple notation, or name required fields. Fields can be of any type.
enum State { Disconnected, Connected (IpAddr) Authorized {service: String, auth: u128}, Pending, }
You can think of enums as unions, but unions that actually know which variant they hold at the moment. Rust enums are an example of an algebraic type or, to be more specific, a sum type. This is because all possible enum values are the sum of all enum variants possible values.
Let’s use enums . To refer to a variant we use:
let mut s = State::Connected(IpAddr::from_str("127.0.0.1").unwrap()); ... s = State::Authorized{service: "/test".to_string(), auth: 260272236571964567899907446074317682114}; … s = State::Disconnected;
To see what value enum contains we can’t simply use comparison. In general, we can’t write:
if s == State::Disconnected { //in general this won’t work println!("Disconnected"); }
This is because enums do not implement trait PartialEq
which is required to allow for comparisons (we will write about traits later).
Note1: in some cases, comparisons made as in the example above may be useful (although they are certainly not idiomatic). To allow this we have to implement PartialEq
which fortunately may be done automatically for us
by
#[derive]
attribute:
#[derive(PartialEq)] enum State {Disconnected} .. //now we can use if s == State::Disconnected { println!("Disconnected"); }
Attribute #[derive]
is used to automatically implement traits for data type. Many commonly used traits (like Debug
, Error
, PartialEq
, Copy
, Clone
, Ord
, ...) can be derived in this way. You can also define your own derive macro which can automatically generate implementation for your own traits.
To see what value enum contains in an idiomatic rust way, we will use pattern matching .
Pattern Matching
Match is an extremely useful language feature. It uses pattern matching to choose from alternative code branches and allows for programming in a clear way, without using multiple if/else clauses.
Using match we can check enums variants and their associated data:
match s { State::Disconnected => println!("Disconnected"), State::Connected(x) => println!("Connected to {}", x), State::Authorized{service: s, auth: a} => println!("Authorized to {} with {}", s, a), _ => println!("unused!") }
Pattern matching
is exhaustive which means all possible values have to be served. When you are interested only in selected values you can use _
to denote ‘all other options’.
But match is not only used to check on enums. You can use match with any data type. Moreover, in pattern matching there are some exciting features, for instance we can:
| ..= @
Take a look at the following examples:
match x { 1 => println!("1"), 2|3 => println!("2,3"), 4 ..= 7 => println!("4,5,6,7"), e @ 11 ..= 15 => println!("element from range 11 ... 15 is {}", e), 20 ..= 100 if x % 2 == 0 => println!("even element from range 20...100"), _ => println!("all other") }
Note1
: if we bind the matched value (above with e
variable) we can overshadow the existing bindings.
Note2 : Ranges are only allowed on numeric and char values, so we can write also:
match y { 'A' ..= 'Z' => println!("Upper case letter"), 'z' ..= 'z' => println!("Lower case letter"), _ => println!("sth else") }
Note3: overlapping ranges are checked, so you can’t for instance do that:
match x { 1|2 => println!("first option!"), 2..=3 => println!("second option!"), //this will fail! 2 is included in previous match _ => println!("all other") }
But beware, patterns are not checked for overlap in general, and the first match is used:
let x = 4; match x { 4 => println!("number 4"), 1..=100 if x % 2 == 0 => println!("even element from range 1...100"), _ => println!("all other") }
The code above results in number 4
, but if we change the order of the matches then:
match x { 1..=100 if x % 2 == 0 => println!("even element from range 1...100"), 4 => println!("number 4"), _ => println!("all other") }
we will get even element from range 1...100
Pattern matching can also be used for unwrapping structure fields (like in the previous enum example) and what is more, we can even skip unused fields in various ways using:
_ ..
All the matches below are correct, and we can fire any of them by changing their order.
struct Color{r: u8, b: u8, g:u8} … match c { Color{b, g, ..} => println!("Color components: g, b: {} {}", g,b), Color{g, ..} => println!("Color component: g: {}", g), Color{r, ..} => println!("Color component: r: {}", r), Color{r: rr, g:_, b: bb} => println!("Color components: r, b {},{}", rr,bb), Color{r,g,b} => println!("Color r:{} g:{} b:{}", r,g,b) }
Note that the matches „overlap” in the sense many of them can match at the same time. The compiler does not forbid this. As we have already mentioned, the match conditions overlapping check applies only to numeric and char ranges.
Match is an expression and as such can be used to return a value:
let c = Color{r: 255,g:0,b:0 }; let y: u8 = match c { Color{r, g, b} if r > 0 => (0.299*(r as f32)+0.587*(g as f32)+0.114*(b as f32)) as u8, _ => 0u8, }; println!("gray scale value: {}", y);
Which gives: gray scale value: 76
. In this example gray scale level is calculated if r
component is nonzero, otherwise the match returns 0u8
(this type of indicating suffix syntax we can use for numeric literals).
Note 4:
The above example looks like field destructuring, but if this is your main purpose you should rather use let
syntax:
let c = Color{r: 255,g:0,b:0 }; … //now destructuring: let Color{r, g, b} = c; let y= (0.299*(r as f32)+0.587*(g as f32)+0.114*(b as f32)) as u8; println!("gray scale value: {}", y);
Note 5: Rust is explicit about used types and in cases such as before numeric type conversions are required – which is done with the usage of keyword ‘ as ’.
If we have tuple struct then matching looks like that:
struct Color(u8,u8,u8); let c = Color(255,0,0); let r: u8 =match c { Color(x,..) if x > 0 => x, _ => 0u8, }; println!("color r component: {}", r);
Option
Rust uses Option type to take care of values that may not exist in your program. Option is an enum type and is defined in the standard library as follows:
pub enum Option<T> { None, Some(T), }
Generics syntax is used in this definition, where
T
represents any type. Let’s say we have Option<u8>
and we will use it to represent u8
values which sometimes can be not available. If a value is not present, we will use None
variant. If a value is known, let’s say it equals 5
, we will use
Some(5)
variant.
Note1: Option<T>
is a generic type and will be instantiated for all types we actually use with
Option
and those instantiated types aren’t equal. For example, Option<u8>
is not the same as Option<Color>
.
Why does Rust introduce Options after all? The main reason is to ensure that all variables always have meaningful values and therefore you can safely refer to them. Although Option introduces an additional layer of abstraction, it forces the language type system to work for us in dealing with possibly non-existent values, saving us from accidental null pointer dereference.
Other languages in such cases allow to use null or not initialized values. But then, when you look at the code nothing helps you to understand if a variable at given point is null or has a value. This dilemma led to introduce pointer and reference distinction in languages like C++. Pointers can be null, therefore you have to program in a defensive way, checking if the pointed value exists. In the case of references, you have to initialize variables and can’t assign null values, but then you are given a guarantee that reference is not null (at least in a well-defined program).
As you may notice, wrapping values in Option is again a very characteristic approach for Rust – when you need a certain - possibly risky feature (null values can be real cures of your code) you must be explicit about it.
Let’s finally use
Options
. You can directly assign option variables with None
or Some(value)
when you want to wrap value in
Option
.
struct Customer { name: String, company: Option<String> } fn main() { let mut text: Option<String> = Some("Hello, world!".to_string()); text = None; let c = Customer{name: "John".to_string(), company: None}; }
Note2
: You may wonder why we do not use Option::None
and Option::Some
, as it is in the case of any other enums. The reason for that is Rust imports for you many commonly used symbols defined in std::prelude
module, one of such imports is std::option::Option::{self, Some, None}
.
You can use Option in function/method to indicate the absence of returned value
fn divide(n: i32, d: i32) -> Option<i32> { if d == 0 {None} else {Some(n/d)} }
Even in this simple example
Option
is used as a simple error handling mechanism in which, when a function defaults, it returns None
– a method well known from good old C where we return 0
or -1
in the case of error.
With Options we can also deal with optional function parameters, although then all calls will require some additional boilerplate code:
fn optional_arg_fn(data: &Option<String>){} .. optional_arg_fn(None); optional_arg_fn(Some("Hello, world!".to_string()));
Note3 : There are some difficulties associated with this approach, mainly:
- value has to be wrapped in Option type
-
you have to provide an argument even if it is
None
You can address both of these problems in a different way.
Option
wrapping tends to stay explicit, therefore you will see occasionally: Some( _some_value_ )
passed as function arguments in your code. The pain can be relieved a little with the implementation of
Default
and
Into
Option
traits for the type you use as an optional parameter. Unfortunately, there is also a cost here that can outweigh benefits. Function signatures have to go generic like that:
fn optional_arg_fn<T: Into< Option<String> > >(data: T){}
But the real-world solution here is not to use optional parameters at all!
To deal with multiple optional parameters you will find builder pattern very useful. This pattern allows you to build objects with convenient methods that can set incrementally all parameters you require at the moment – see – no need for optional parameters!
struct Foo{} // type we want to build struct FooBuilder{} //builder let f : Foo = FooBuilder::new() .with_baz(...) .with_bar(...) .from_foo(...) .build();
Builder pattern requires some effort to implement, and as such it does not provide the answer for tooling up functions with a single or small number of optional parameters. In such cases if you really don’t want to expose Option in your API, you can simply hide optionality by providing additional public functions:
//private function fn foo_optional_data(required: u32, data: Option<String>){} //public api functions pub fn foo(required: u32){ foo_optional_arg(required, None); } pub fn foo_with_data(required: u32, data: String){ foo_optional_arg(required, Some(data)); }
Now we have an idea how to declare Option usage and how to wrap data into Options , but how to check Options for wrapped data? How to unwrap it and is it always necessary?
To check on wrapped value we can use pattern matching
match divide(10,2) { None => println!("Cant divide!"), Some(d) => println!("result is: {}", d), }
When we need better flow control, we can use let Some
expression.
if let Some(d) = divide(10,2) { println!("result is: {}", d); }
Note 4:We can also unwrap
Option
explicitly to take a value, but it will panic and end your program immediately in case of None
value.
let text: Option<String> = Some("Hello, world!".to_string()); println!("value is: {}", text.unwrap());
You absolutely don't want to do it unless you have an unrecoverable error. And even then - think twice, because what seems to you as an unrecoverable error, can look like a practical problem to be handled for your API user. Option unwrapping may be useful though in the case of small pet projects.
Option
often don’t require unwrapping at all. Standard library provides handy methods (
combinators
) to work with options. Most important of them are map
and and_then
.
Method map
allows us to apply function f : T → U
to Option<T>
and return Option<U>
. It is implemented as:
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> { match self { Some(x) => Some(f(x)), None => None, } }
For now, please skip the intricacies of method signature which is mostly based on generic syntax.
You can think of map
as a convenient method to apply to
Option
wrapped value function which has always a result (cannot fail thus has no
Option
as a result). When you need to use a function that can possibly produce no value (in other words has
Option
as a result type) use and_then
combinator.
Method and_then
takes a function f: T → Option<U>
and allows it to be applied on Option<T>
implementation looks like that:
pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> { match self { Some(x) => f(x), None => None, } }
It’s worth noticing that chained calls of multiple and_then
and map
give None
if None
is produced at any step.
Combinators and_then
and map
but also many others (defined in libcore/option.rs
of Rust sources) allow to build sophisticated pipelines without explicitly unwrapping
Option
values in your code.
//simple pipeline build from map combinators fn compute_or_none(n: Option<i32>) -> Option<String> { n.map(|x| x*x) .map(|x| x+1) .map(|x| format!("{:x}", x) ) } … let divisor = 2; if let Some(d) = compute_or_none(divide(100,divisor)) { println!("result is: {}", d); }else{ println!("result is: None"); }
The pipeline compute_or_none
with the use of a sequence of map transformations calculates (x*x+1)
and then represents the outcome as a hexadecimal number. All that without even thinking of
Option
value.
Note that when the divisor is set to 2 (as above) the code execution gives: result is: 9c5
, but when we set it to 0 we will get: result is: None
.
Note 5
: Syntax |x| x+1
is used to define closure – a simple unnamed function which can also use other variables available in current scope. Of course, combinators can use named functions in which case we would have
fn square(x: i32)->i32{x*x} fn square_or_none(n: Option<i32>) -> Option<String> { n.map(square) //use of named function .map(|x| x+1) .map(|x| format!("{:x}", x) ) }
In the case of transformations that can have no result, as it was said earlier, we should use and_then
in our pipelines. Let’s look at the example:
fn compute_or_none(n: Option<i32>) -> Option<String> { n.map(|x| x as f64) .and_then(|x| { match x { x if x >= 0f64 => Some(x.sqrt()), _ => None } }).map(|x| format!("{}", x) ) } … let divisor = 10; if let Some(d) = compute_or_none(divide(100,divisor)) { println!("result is: {}", d); }else{ println!("result is: None"); }
Again, if we set the divisor to 10
above, we will get result is: 3.1622776601683795
. But if we set the divisor to 0
(result of divide will be None
) or if we use a negative divisor (result of and_then
will be None
) the outcome of the whole compute_or_none
pipeline will be None
and the code execution will give: result is: None
.
Complex applications pipelines, which are illustrated by those somewhat artificial examples, can be hundreds of lines long and, what is important here, no explicit Option unwrapping is used.
That’s all for this part. Stay tuned, the next article coming soon!
You can follow us on Twitter and LinkedIn to be up to date with new posts.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
新内容创业:我这样打造爆款IP
南立新、曲琳 / 机械工业出版社 / 2016-5-10 / 39.00
这是个内容创业爆棚的时代,在采访几十家内容创业公司,与一线最优秀的创业者独家对话之后,作者写作了这本书,其中包括对这个行业的真诚感触,以及希望沉淀下来的体系化思考。 本书共分三个部分讲述了爆红大号的内容创业模式和方法。其中第一部分,讲述了新的生产方式,即内容形态发展的现状--正在被塑造;第二部分,讲述了新的盈利探索,即从贩卖产品到贩卖内容的转变,该部分以多个案例进行佐证,内容翔实;第三部分,......一起来看看 《新内容创业:我这样打造爆款IP》 这本书的介绍吧!