引言 在 DICOM 医疗影像系统中,数据的准确性和一致性至关重要。本文将深入探讨如何使用 Rust 的类型系统来创建自定义的安全类型封装,以防止运行时错误并提高代码的可维护性。
BoundedString:长度限制字符串 BoundedString<N> 是一个泛型结构体,用于确保字符串长度不超过指定的 N 个字符。
FixedLengthString:固定长度字符串 FixedLengthString<N> 确保字符串长度恰好为 N 个字符,这在处理某些 DICOM 字段时非常有用。
DicomDateString:DICOM 日期格式 DICOM 标准要求日期字段使用 YYYYMMDD 格式,DicomDateString 类型专门用于处理这种格式。
类型系统的优势 1. 编译时保证 通过这些自定义类型,许多数据验证错误可以在编译时就被发现,而不是在运行时才暴露。
2. 明确的意图表达 类型名称本身就表达了数据的约束条件,使代码更具可读性和自文档化。
3. 防止错误传递 一旦数据通过类型验证,后续代码可以假设数据格式是正确的,无需重复验证。
具体实现: 1.dicom_dbtype.rs
use serde::{Deserialize, Serialize}; use snafu::Snafu; use std::fmt; use std::hash::Hash; #[derive(Debug, Snafu)] #[non_exhaustive] pub enum BoundedStringError { #[snafu(display("String too long: {} > {}", len, max))] TooLong { max: usize, len: usize }, #[snafu(display("String length is: {} and expected: {}", len, fixlen))] LengthError { fixlen: usize, len: usize }, } type BoundedResult<T, E = BoundedStringError> = Result<T, E>; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] #[derive(Default)] pub struct BoundedString<const N: usize> { value: String, } impl<const N: usize> fmt::Display for BoundedString<N> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.value) } } impl<const N: usize> BoundedString<N> { pub fn new(s: String) -> BoundedResult<BoundedString<N>> { if s.len() > N { Err(BoundedStringError::TooLong { max: N, len: s.len(), }) } else { Ok(Self { value: s }) } } pub fn from_str(s: &str) -> BoundedResult<BoundedString<N>> { if s.len() > N { Err(BoundedStringError::TooLong { max: N, len: s.len(), }) } else { Ok(Self { value: s.to_string(), }) } } pub fn from_string(s: &String) -> BoundedResult<BoundedString<N>> { if s.len() > N { Err(BoundedStringError::TooLong { max: N, len: s.len(), }) } else { Ok(Self { value: s.clone() }) } } pub fn as_str(&self) -> &str { &self.value } // 使用 deref 方式访问 pub fn as_ref(&self) -> &String { &self.value } } impl<const N: usize> Hash for BoundedString<N> { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.value.hash(state); } } impl<const N: usize> PartialEq for BoundedString<N> { fn eq(&self, other: &Self) -> bool { self.value == other.value } } impl<const N: usize> Eq for BoundedString<N> {} impl<const N: usize> TryFrom<&str> for BoundedString<N> { type Error = BoundedStringError; fn try_from(s: &str) -> BoundedResult<Self> { BoundedString::from_str(s) } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct FixedLengthString<const N: usize> { pub(crate) value: String, } impl<const N: usize> Hash for FixedLengthString<N> { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.value.hash(state); } } impl<const N: usize> PartialEq for FixedLengthString<N> { fn eq(&self, other: &Self) -> bool { self.value == other.value } } impl<const N: usize> Eq for FixedLengthString<N> {} /// DICOM文件中的表示日期的字符串,格式为 YYYYMMDD, 长度为 8, 例如 "20231005" #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(transparent)] pub struct DicomDateString { pub(crate) value: String, } // 为 DicomDateString 实现 Display trait impl fmt::Display for DicomDateString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.value) } } impl TryFrom<&str> for DicomDateString { type Error = BoundedStringError; fn try_from(s: &str) -> BoundedResult<Self> { // 使用 NaiveDate 验证日期格式和有效性 chrono::NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(|_| { BoundedStringError::LengthError { fixlen: 8, len: s.len(), } })?; Ok(Self { value: s.to_string(), }) } } impl TryFrom<&String> for DicomDateString { type Error = BoundedStringError; fn try_from(s: &String) -> BoundedResult<Self> { // 使用 NaiveDate 验证日期格式和有效性 chrono::NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(|_| { BoundedStringError::LengthError { fixlen: 8, len: s.len(), } })?; Ok(Self { value: s.clone() }) } } impl<const N: usize> FixedLengthString<N> { pub fn new(s: String) -> BoundedResult<FixedLengthString<N>> { if s.len() != N { Err(BoundedStringError::LengthError { fixlen: N, len: s.len(), }) } else { Ok(Self { value: s }) } } pub fn from_str(s: &str) -> BoundedResult<FixedLengthString<N>> { if s.len() != N { Err(BoundedStringError::LengthError { fixlen: N, len: s.len(), }) } else { Ok(Self { value: s.to_string(), }) } } pub fn from_string(s: &String) -> BoundedResult<FixedLengthString<N>> { if s.len() != N { Err(BoundedStringError::LengthError { fixlen: N, len: s.len(), }) } else { Ok(Self { value: s.clone() }) } } pub fn as_str(&self) -> &str { &self.value } } impl DicomDateString { pub fn as_str(&self) -> &str { self.value.as_str() } pub(crate) fn from_db(s: &str) -> Self { // 使用 NaiveDate 验证日期格式和有效性 chrono::NaiveDate::parse_from_str(&s, "%Y%m%d") .map_err(|_| BoundedStringError::LengthError { fixlen: 8, len: s.len(), }) .expect( format!( "DicomDateString::make_from_db only support YYYYMMDD format, but got {}", s ) .as_str(), ); Self { value: s.to_string(), } } } dicom_meta.rs
...