Я новичок в Rust и учусь, реализуя свою собственную версию cut
. Это фрагмент, который анализирует <list>
диапазонов, необходимых для -f
, -b
, или же -c
опции. В соответствующем разделе спецификации говорится:
Приложение должно гарантировать, что список аргументов-опций (см. Параметры -b, -c и -f ниже) является разделенным списком или разделенным списком положительных чисел и диапазонов. Диапазоны могут быть трех видов. Первое — это два положительных числа, разделенных знаком (от низкого до высокого), которые представляют все поля от первого до второго числа. Второе — положительное число, которому предшествует (- высокий), который представляет все поля от номера 1 до этого номера. Третье — положительное число, за которым следует (low-), которое представляет это число до последнего поля включительно. Элементы в списке могут повторяться, перекрываться и указываться в любом порядке, но выбранные байты, символы или поля должны быть записаны в порядке входных данных. Если элемент появляется в списке выбора более одного раза, он должен быть записан ровно один раз.
Меня интересуют советы по написанию более идиоматического Rust (особенно обработка ошибок) и любые другие советы для начинающих программистов на Rust. Спасибо!
use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::iter::FromIterator;
use std::num::ParseIntError;
pub type Result<T> = std::result::Result<T, RangeError>;
#[derive(Debug, Eq, PartialEq)]
pub enum RangeError {
MalformedRangSpec,
Parse(ParseIntError),
}
impl Display for RangeError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use RangeError::*;
let msg = match self {
MalformedRangSpec => format!("Invalid range spec"),
Parse(e) => e.to_string(),
};
write!(f, "Error: {}", msg)
}
}
impl From<ParseIntError> for RangeError {
fn from(e: ParseIntError) -> Self {
RangeError::Parse(e)
}
}
impl Error for RangeError {}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Range {
From(usize),
To(usize),
Inclusive(usize, usize),
Singleton(usize),
}
#[derive(Debug, Eq, PartialEq)]
pub struct RangeSet {
ranges: Vec<Range>,
}
impl RangeSet {
pub fn from<I: IntoIterator<Item = Range>>(iter: I) -> RangeSet {
RangeSet {
ranges: Vec::from_iter(iter),
}
}
pub fn from_spec<T: AsRef<str>>(spec: T) -> Result<RangeSet> {
// "-5,10,14-17,20-"
let tuples = spec
.as_ref()
.split(|c| c == ',' || c == ' ') // e.g. ["-5", "10", "14-17", "20-"]
// [[None, Some(5)], [Some(10)], [Some(14), Some(17)], [Some(20), None]]
.map(|element| {
element
.split('-') // e.g. first iter: ["", 5]
.map(|bound| match bound {
// e.g. [None, Some("5")]
"" => Ok(None),
s => {
let n: usize = s.parse()?;
Ok(Some(n))
}
})
.collect::<Result<Vec<_>>>()
})
.collect::<Result<Vec<_>>>()?;
let ranges: Vec<Range> = tuples
.iter()
.map(|range| match range.as_slice() {
[Some(n)] => Ok(Range::Singleton(*n)),
[Some(s), Some(e)] => Ok(Range::Inclusive(*s, *e)),
[Some(s), None] => Ok(Range::From(*s)),
[None, Some(e)] => Ok(Range::To(*e)),
_ => Err(RangeError::MalformedRangSpec),
})
.collect::<Result<Vec<_>>>()?;
Ok(RangeSet::from(ranges))
}
pub fn contains(&self, n: usize) -> bool {
if n == 0 {
// range defined to start at 1
return false;
}
self.ranges.iter().any(|range| match range {
Range::From(from) => (*from..).contains(&n),
Range::To(to) => (1..=*to).contains(&n),
Range::Inclusive(from, to) => (*from..=*to).contains(&n),
Range::Singleton(s) => s == &n,
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn contains() {
let r = RangeSet::from(vec![
Range::From(100),
Range::Inclusive(50, 60),
Range::Singleton(40),
Range::To(10),
]);
for n in 0..1000 {
match n {
1..=10 | 40..=40 | 50..=60 | 100..=1000 => {
assert!(r.contains(n), "should contain {}", n)
}
_ => assert!(!r.contains(n), "shouldn't contain {}", n),
}
}
}
#[test]
fn from_spec() {
let r1 = RangeSet::from(vec![Range::Singleton(1)]);
let r2 = RangeSet::from_spec("1");
assert_eq!(Ok(r1), r2);
let r1 = RangeSet::from(vec![
Range::To(10),
Range::Singleton(40),
Range::Inclusive(50, 60),
Range::From(100),
]);
let r2 = RangeSet::from_spec("-10,40,50-60,100-");
assert_eq!(Ok(r1), r2);
}
#[test]
fn from_spec_bad() {
assert!(RangeSet::from_spec("b").is_err());
assert!(RangeSet::from_spec("4-5-6").is_err());
}
}
1 ответ
Хорошее использование пользовательских типов RangeError
, Range
, и RangeSet
. Некоторые отзывы:
Самая большая проблема с вашим кодом, которую я вижу, заключается в том, что
RangeSet::from_spec
функция довольно грубая. При осмотре мой диагноз проблемы заключается в том, что вы реализуете функциональность вRangeSet
это было бы лучше реализовать наRange
. Вот такой рефакторинг:impl FromStr for Range { type Err = RangeError; fn from_str(s: &str) -> Result<Range> { let bounds = s .split('-') .map(|bound| match bound { "" => Ok(None), s => Ok(Some(s.parse()?)) }) .collect::<Result<Vec<_>>>()?; match bounds.as_slice() { [Some(n)] => Ok(Range::Singleton(*n)), [Some(s), Some(e)] => Ok(Range::Inclusive(*s, *e)), [Some(s), None] => Ok(Range::From(*s)), [None, Some(e)] => Ok(Range::To(*e)), _ => Err(RangeError::MalformedRangSpec), } } } impl Range { pub fn contains(&self, n: usize) -> bool { if n == 0 { return false; } match self { Range::From(from) => (*from..).contains(&n), Range::To(to) => (1..=*to).contains(&n), Range::Inclusive(from, to) => (*from..=*to).contains(&n), Range::Singleton(s) => s == &n, } } }
В
match
заявления, вы также можете убрать все эти посторонние*
s путем сопоставления с образцом наmatch *x
вместоmatch x
, вот как это выглядит:impl Range { pub fn contains(&self, n: usize) -> bool { if n == 0 { return false; } match *self { Range::From(from) => (from..).contains(&n), Range::To(to) => (1..=to).contains(&n), Range::Inclusive(from, to) => (from..=to).contains(&n), Range::Singleton(s) => s == n, } } }
и аналогично в
fn from_str
:match *bounds.as_slice()
и[Some(n)] => Ok(Range::Singleton(n))
и Т. Д.в
&[Some(s), Some(e)] => Ok(Range::Inclusive(s, e))
случай, возможно, вы хотите проверить, что диапазон нетривиален:[Some(s), Some(e)] => { if s < e { Ok(Range::Inclusive(s, e)) } else { Err(RangeError::MalformedRangSpec) } },
С этими реализациями на
Range
,RangeSet
становится намного проще:impl RangeSet { pub fn from<I: IntoIterator<Item = Range>>(iter: I) -> RangeSet { RangeSet { ranges: Vec::from_iter(iter), } } pub fn from_spec<T: AsRef<str>>(spec: T) -> RangeResult<RangeSet> { // "-5,10,14-17,20-" let ranges = spec .as_ref() .split(|c| c == ',' || c == ' ') // e.g. ["-5", "10", "14-17", "20-"] .map(|element| element.parse()) .collect::<RangeResult<Vec<_>>>()?; Ok(RangeSet::from(ranges)) } pub fn contains(&self, n: usize) -> bool { self.ranges.iter().any(|range| range.contains(n)) } }
В вашей реализации в настоящее время отсутствует интерфейс командной строки. Для этого вы, вероятно, захотите использовать structopt ящик. Стоит учесть, что это может сделать некоторые из ваших функций, в частности
RangeSet::from_spec
ненужный. В частности, если вы определяете аргумент командной строки типаVec<Range>
StructOpt автоматически определит разумный способ разбора строки на вектор диапазонов (это зависит отRange
реализацияFromStr
что мы и сделали выше).В
RangeError
реализация — это идиоматический способ обработки ошибок, и он хорошо выглядит. Однако я лично предпочел бы не переопределять стандартные типы библиотек, такие какpub type Result<T> = std::result::Result<T, RangeError>
, поскольку это приводит к нестандартным сигнатурам функций, которые нельзя сразу прочитать. Тип ошибки является частью поведения функции, поэтому лучше явно возвращатьResult<T, RangeError>
в ваших функциях. В качестве альтернативы вы можете просто использовать новое имя:pub type RangeResult<T> = Result<Range, RangeError>
(FWIW, я тоже думаю
std::fmt::Result
иstd::io::Result<T>
были ошибки.)
Окончательный код:
use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::iter::FromIterator;
use std::num::ParseIntError;
use std::str::FromStr;
pub type RangeResult<T> = std::result::Result<T, RangeError>;
#[derive(Debug, Eq, PartialEq)]
pub enum RangeError {
MalformedRangSpec,
Parse(ParseIntError),
}
impl Display for RangeError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use RangeError::*;
let msg = match self {
MalformedRangSpec => "Invalid range spec".to_string(),
Parse(e) => e.to_string(),
};
write!(f, "Error: {}", msg)
}
}
impl From<ParseIntError> for RangeError {
fn from(e: ParseIntError) -> Self {
RangeError::Parse(e)
}
}
impl Error for RangeError {}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Range {
From(usize),
To(usize),
Inclusive(usize, usize),
Singleton(usize),
}
// Added
impl FromStr for Range {
type Err = RangeError;
fn from_str(s: &str) -> RangeResult<Range> {
let bounds = s
.split('-')
.map(|bound| match bound {
"" => Ok(None),
s => Ok(Some(s.parse()?))
})
.collect::<RangeResult<Vec<_>>>()?;
match *bounds.as_slice() {
[Some(n)] => Ok(Range::Singleton(n)),
[Some(s), Some(e)] => {
if s < e {
Ok(Range::Inclusive(s, e))
} else {
Err(RangeError::MalformedRangSpec)
}
},
[Some(s), None] => Ok(Range::From(s)),
[None, Some(e)] => Ok(Range::To(e)),
_ => Err(RangeError::MalformedRangSpec),
}
}
}
impl Range {
pub fn contains(&self, n: usize) -> bool {
if n == 0 {
return false;
}
match *self {
Range::From(from) => (from..).contains(&n),
Range::To(to) => (1..=to).contains(&n),
Range::Inclusive(from, to) => (from..=to).contains(&n),
Range::Singleton(s) => s == n,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct RangeSet {
ranges: Vec<Range>,
}
impl RangeSet {
pub fn from<I: IntoIterator<Item = Range>>(iter: I) -> RangeSet {
RangeSet {
ranges: Vec::from_iter(iter),
}
}
pub fn from_spec<T: AsRef<str>>(spec: T) -> RangeResult<RangeSet> {
// "-5,10,14-17,20-"
let ranges = spec
.as_ref()
.split(|c| c == ',' || c == ' ')
// e.g. ["-5", "10", "14-17", "20-"]
.map(|element| element.parse())
.collect::<RangeResult<Vec<_>>>()?;
Ok(RangeSet::from(ranges))
}
pub fn contains(&self, n: usize) -> bool {
self.ranges.iter().any(|range| range.contains(n))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn contains() {
let r = RangeSet::from(vec![
Range::From(100),
Range::Inclusive(50, 60),
Range::Singleton(40),
Range::To(10),
]);
for n in 0..1000 {
match n {
1..=10 | 40..=40 | 50..=60 | 100..=1000 => {
assert!(r.contains(n), "should contain {}", n)
}
_ => assert!(!r.contains(n), "shouldn't contain {}", n),
}
}
}
#[test]
fn from_spec() {
let r1 = RangeSet::from(vec![Range::Singleton(1)]);
let r2 = RangeSet::from_spec("1");
assert_eq!(Ok(r1), r2);
let r1 = RangeSet::from(vec![
Range::To(10),
Range::Singleton(40),
Range::Inclusive(50, 60),
Range::From(100),
]);
let r2 = RangeSet::from_spec("-10,40,50-60,100-");
assert_eq!(Ok(r1), r2);
}
#[test]
fn from_spec_bad() {
assert!(RangeSet::from_spec("b").is_err());
assert!(RangeSet::from_spec("4-5-6").is_err());
}
}
Спасибо за понимание! «(FWIW, я также считаю, что std :: fmt :: Result и std :: io :: Result были ошибками.)« Я тоже подумал, что это сбивает с толку, но, похоже, это было условностью. Мне больше нравится ваше предложение.
— слебарон