rust-traits - 解答
実装コード
カスタムトレイト定義
// src/traits.rs
use std::fmt;
/// 基本的なドキュメントトレイト
pub trait Document {
fn title(&self) -> &str;
fn content(&self) -> &str;
/// デフォルト実装
fn summary(&self) -> String {
let content = self.content();
let truncated = if content.len() > 50 {
&content[..50]
} else {
content
};
format!("{}: {}...", self.title(), truncated)
}
fn word_count(&self) -> usize {
self.content().split_whitespace().count()
}
}
/// 関連型を持つパーサートレイト
pub trait Parser {
type Output;
type Error: std::error::Error;
fn parse(&self, input: &str) -> Result<Self::Output, Self::Error>;
}
/// シリアライズ可能なトレイト
pub trait Serializable {
fn serialize(&self) -> String;
fn deserialize(data: &str) -> Result<Self, String>
where
Self: Sized;
}
/// 検証可能なトレイト
pub trait Validatable {
fn is_valid(&self) -> bool;
fn validate(&self) -> Result<(), Vec<String>>;
}
構造体と実装
// src/impls.rs
use crate::traits::{Document, Serializable, Validatable};
use std::fmt;
#[derive(Clone)]
pub struct Article {
pub title: String,
pub author: String,
pub body: String,
}
impl Article {
pub fn new(title: &str, author: &str, body: &str) -> Self {
Article {
title: title.to_string(),
author: author.to_string(),
body: body.to_string(),
}
}
}
// Document トレイトの実装
impl Document for Article {
fn title(&self) -> &str {
&self.title
}
fn content(&self) -> &str {
&self.body
}
}
// Display トレイトの実装
impl fmt::Display for Article {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"「{}」by {}\n\n{}",
self.title, self.author, self.body
)
}
}
// Debug トレイトの実装
impl fmt::Debug for Article {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Article")
.field("title", &self.title)
.field("author", &self.author)
.field("body_len", &self.body.len())
.finish()
}
}
// From<&str> の実装
impl From<&str> for Article {
fn from(s: &str) -> Self {
Article {
title: "Quick Note".to_string(),
author: "Anonymous".to_string(),
body: s.to_string(),
}
}
}
// Serializable の実装
impl Serializable for Article {
fn serialize(&self) -> String {
format!(
"TITLE:{}\nAUTHOR:{}\nBODY:{}",
self.title, self.author, self.body
)
}
fn deserialize(data: &str) -> Result<Self, String> {
let mut title = None;
let mut author = None;
let mut body = None;
for line in data.lines() {
if let Some(t) = line.strip_prefix("TITLE:") {
title = Some(t.to_string());
} else if let Some(a) = line.strip_prefix("AUTHOR:") {
author = Some(a.to_string());
} else if let Some(b) = line.strip_prefix("BODY:") {
body = Some(b.to_string());
}
}
match (title, author, body) {
(Some(t), Some(a), Some(b)) => Ok(Article {
title: t,
author: a,
body: b,
}),
_ => Err("Invalid format".to_string()),
}
}
}
// Validatable の実装
impl Validatable for Article {
fn is_valid(&self) -> bool {
!self.title.is_empty() && !self.author.is_empty()
}
fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.title.is_empty() {
errors.push("Title cannot be empty".to_string());
}
if self.author.is_empty() {
errors.push("Author cannot be empty".to_string());
}
if self.body.len() < 10 {
errors.push("Body must be at least 10 characters".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
ジェネリック関数
// src/generics.rs
use crate::traits::Document;
/// 単一のトレイト境界
pub fn process<T: Document + Clone>(doc: T) -> String {
let cloned = doc.clone();
format!(
"Processed: {} ({} words)",
cloned.title(),
cloned.word_count()
)
}
/// where句を使用した複数境界
pub fn merge<T, U>(a: T, b: U) -> String
where
T: Document,
U: Document,
{
format!(
"{}\n---\n{}",
a.content(),
b.content()
)
}
/// トレイトオブジェクトを受け取る
pub fn print_all(docs: &[&dyn Document]) {
for doc in docs {
println!("{}", doc.summary());
}
}
/// 複数のトレイト境界
pub fn detailed_process<T>(doc: T) -> String
where
T: Document + std::fmt::Display + Clone,
{
format!("Document:\n{}\n\nSummary: {}", doc, doc.summary())
}
テストコード
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_article_document() {
let article = Article::new("Title", "Author", "This is the body content");
assert_eq!(article.title(), "Title");
assert_eq!(article.word_count(), 5);
}
#[test]
fn test_article_display() {
let article = Article::new("Test", "Tester", "Content");
let display = format!("{}", article);
assert!(display.contains("Test"));
assert!(display.contains("Tester"));
}
#[test]
fn test_article_from_str() {
let article: Article = "Hello world".into();
assert_eq!(article.body, "Hello world");
}
#[test]
fn test_serialization() {
let article = Article::new("T", "A", "B");
let serialized = article.serialize();
let deserialized = Article::deserialize(&serialized).unwrap();
assert_eq!(article.title, deserialized.title);
}
#[test]
fn test_validation() {
let valid = Article::new("Title", "Author", "Long enough body");
assert!(valid.is_valid());
let invalid = Article::new("", "Author", "Body");
assert!(!invalid.is_valid());
}
#[test]
fn test_generic_process() {
let article = Article::new("Test", "A", "Content here");
let result = process(article);
assert!(result.contains("Test"));
}
#[test]
fn test_merge() {
let a = Article::new("A", "X", "Content A");
let b = Article::new("B", "Y", "Content B");
let merged = merge(a, b);
assert!(merged.contains("Content A"));
assert!(merged.contains("Content B"));
}
}