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"));
    }
}