課題6: ライフタイムの実践
マンダトリー要件
問題1:ライフタイムの理解(20点)
以下のコードについて、コンパイルエラーが発生するか判定し、理由を説明しなさい。
// 1-1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("{}", result);
}
// 1-2
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael.");
let excerpt = Excerpt {
part: &novel[..4],
};
drop(novel);
println!("{}", excerpt.part);
}
// 1-3
fn dangle() -> &str {
let s = String::from("hello");
&s
}
// 1-4
fn first_word(s: &str) -> &str {
&s[..1]
}
// 1-5
struct Context<'a> {
text: &'a str,
}
impl<'a> Context<'a> {
fn get_text(&self) -> &str {
self.text
}
}
// 1-6
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
fn main() {
let string1 = String::from("long");
{
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("{}", result);
}
}
各コードについて:
- コンパイルできるか?
- エラーの場合、どのライフタイムルールに違反しているか?
- どう修正するか?
提出物:problem1_lifetime_analysis.md
問題2:ライフタイム注釈の実装(20点)
以下の関数にライフタイム注釈を追加し、実装しなさい。
// 2つの文字列のうち長い方を返す
fn longest(x: &str, y: &str) -> &str {
// TODO: ライフタイム注釈を追加
if x.len() > y.len() {
x
} else {
y
}
}
// 最初の単語と残りを返す
fn split_first_word(text: &str) -> (&str, &str) {
// TODO: ライフタイム注釈を追加
match text.find(' ') {
Some(pos) => (&text[..pos], &text[pos + 1..]),
None => (text, ""),
}
}
// テキストから最長の行を返す
fn longest_line(text: &str) -> &str {
// TODO: ライフタイム注釈を追加
text.lines()
.max_by_key(|line| line.len())
.unwrap_or("")
}
// 2つの参照のうち最初のものを返す
fn first_ref<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
// TODO: 実装
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longest() {
assert_eq!(longest("hello", "world!"), "world!");
assert_eq!(longest("rust", "go"), "rust");
}
#[test]
fn test_split_first_word() {
assert_eq!(split_first_word("hello world"), ("hello", "world"));
assert_eq!(split_first_word("rust"), ("rust", ""));
}
#[test]
fn test_longest_line() {
let text = "short\nthis is a longer line\nmedium";
assert_eq!(longest_line(text), "this is a longer line");
}
#[test]
fn test_first_ref() {
let x = "first";
let y = "second";
assert_eq!(first_ref(x, y), "first");
}
}
提出物:problem2_lifetime_functions.rs
問題3:構造体とライフタイム(20点)
ライフタイムを使った構造体を実装しなさい。
// テキストの一部を保持する構造体
struct TextExcerpt<'a> {
text: &'a str,
}
impl<'a> TextExcerpt<'a> {
// 新しいExcerptを作成
fn new(text: &'a str) -> Self {
// TODO: 実装
}
// テキストを取得
fn get_text(&self) -> &str {
// TODO: 実装
}
// 最初のn文字を返す
fn first_n_chars(&self, n: usize) -> &str {
// TODO: 実装
}
// 最初の行を返す
fn first_line(&self) -> &str {
// TODO: 実装
}
// 単語数を返す
fn word_count(&self) -> usize {
// TODO: 実装
}
}
// 2つの参照を保持する構造体
struct Pair<'a, 'b> {
first: &'a str,
second: &'b str,
}
impl<'a, 'b> Pair<'a, 'b> {
fn new(first: &'a str, second: &'b str) -> Self {
// TODO: 実装
}
fn get_first(&self) -> &str {
// TODO: 実装
}
fn get_second(&self) -> &str {
// TODO: 実装
}
fn longer(&self) -> &str {
// TODO: 実装
// ヒント: 返り値のライフタイムはどうなる?
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_excerpt() {
let text = String::from("Hello, Rust programming!");
let excerpt = TextExcerpt::new(&text);
assert_eq!(excerpt.get_text(), "Hello, Rust programming!");
assert_eq!(excerpt.first_n_chars(5), "Hello");
assert_eq!(excerpt.word_count(), 3);
}
#[test]
fn test_pair() {
let s1 = String::from("short");
let s2 = String::from("longer string");
let pair = Pair::new(&s1, &s2);
assert_eq!(pair.get_first(), "short");
assert_eq!(pair.get_second(), "longer string");
assert_eq!(pair.longer(), "longer string");
}
}
提出物:problem3_lifetime_structs.rs
問題4:実践的なライフタイム活用(20点)
テキスト検索とパース機能を実装しなさい。
// テキスト内でパターンにマッチする行を返す
fn find_lines<'a>(text: &'a str, pattern: &str) -> Vec<&'a str> {
// TODO: 実装
// ヒント: patternにはライフタイム不要(返り値に含まれない)
}
// キーと値のペアをパース
fn parse_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
// TODO: 実装
// 形式: "key=value"
}
// CSVの行をパース
fn parse_csv_line<'a>(line: &'a str) -> Vec<&'a str> {
// TODO: 実装
// カンマで分割
}
// 複数行のテキストから指定範囲を抽出
fn extract_lines<'a>(text: &'a str, start: usize, end: usize) -> Vec<&'a str> {
// TODO: 実装
}
// URLからドメインを抽出
fn extract_domain<'a>(url: &'a str) -> Option<&'a str> {
// TODO: 実装
// 例: "https://example.com/path" -> "example.com"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_lines() {
let text = "hello world\nrust programming\nhello rust";
let lines = find_lines(text, "hello");
assert_eq!(lines, vec!["hello world", "hello rust"]);
}
#[test]
fn test_parse_key_value() {
assert_eq!(parse_key_value("name=Alice"), Some(("name", "Alice")));
assert_eq!(parse_key_value("invalid"), None);
}
#[test]
fn test_parse_csv_line() {
let csv = "Alice,30,Engineer";
assert_eq!(parse_csv_line(csv), vec!["Alice", "30", "Engineer"]);
}
#[test]
fn test_extract_lines() {
let text = "line1\nline2\nline3\nline4";
assert_eq!(extract_lines(text, 1, 3), vec!["line2", "line3"]);
}
#[test]
fn test_extract_domain() {
assert_eq!(extract_domain("https://example.com/path"), Some("example.com"));
assert_eq!(extract_domain("invalid"), None);
}
}
提出物:problem4_lifetime_parsing.rs
---
ボーナス課題
> ボーナス: 以下はオプションです。マンダトリー要件を完了してから挑戦してください。
ボーナス1:設定ファイルパーサー(5点)
INI形式の設定ファイルパーサーを実装しなさい。
use std::collections::HashMap;
struct Config<'a> {
sections: HashMap<&'a str, HashMap<&'a str, &'a str>>,
}
impl<'a> Config<'a> {
fn new() -> Self {
Config {
sections: HashMap::new(),
}
}
// INIファイルをパース
fn parse(text: &'a str) -> Self {
// TODO: 実装
// 形式:
// [section]
// key=value
}
// セクションの値を取得
fn get(&self, section: &str, key: &str) -> Option<&str> {
// TODO: 実装
}
// セクション一覧を取得
fn sections(&self) -> Vec<&str> {
// TODO: 実装
}
}
fn main() {
let config_text = r#"
[database]
host=localhost
port=5432
[server]
host=0.0.0.0
port=8080
"#;
let config = Config::parse(config_text);
if let Some(host) = config.get("database", "host") {
println!("Database host: {}", host);
}
}
提出物:bonus1_config_parser/プロジェクト
ボーナス2:トークナイザー(5点)
テキストをトークンに分割するライブラリを実装しなさい。
#[derive(Debug, PartialEq)]
enum Token<'a> {
Word(&'a str),
Number(&'a str),
Symbol(char),
Whitespace,
}
struct Tokenizer<'a> {
input: &'a str,
position: usize,
}
impl<'a> Tokenizer<'a> {
fn new(input: &'a str) -> Self {
Tokenizer { input, position: 0 }
}
// 次のトークンを取得
fn next_token(&mut self) -> Option<Token<'a>> {
// TODO: 実装
}
// 全トークンを取得
fn tokenize(input: &'a str) -> Vec<Token<'a>> {
// TODO: 実装
}
}
fn main() {
let text = "hello 123 + world";
let tokens = Tokenizer::tokenize(text);
for token in tokens {
println!("{:?}", token);
}
}
提出物:bonus2_tokenizer/プロジェクト
ボーナス3:Markdown パーサー(5点)
簡易Markdownパーサーを実装しなさい。
#[derive(Debug, PartialEq)]
enum MarkdownElement<'a> {
Heading { level: usize, text: &'a str },
Paragraph(&'a str),
CodeBlock { language: Option<&'a str>, code: &'a str },
ListItem(&'a str),
}
fn parse_markdown<'a>(text: &'a str) -> Vec<MarkdownElement<'a>> {
// TODO: 実装
// サポート:
// # Heading
// ## Heading 2
// Normal paragraph
// rust
// code
// // - List item
}
fn main() {
let markdown = r#"
# Title
This is a paragraph.
## Code Example
rust
fn main() {}
- Item 1
- Item 2
"#;
let elements = parse_markdown(markdown);
for elem in elements {
println!("{:?}", elem);
}
}
提出物:bonus3_markdown_parser/プロジェクト
ボーナス4:テンプレートエンジン(5点)
シンプルなテンプレートエンジンを実装しなさい。
use std::collections::HashMap;
struct Template<'a> {
template: &'a str,
}
impl<'a> Template<'a> {
fn new(template: &'a str) -> Self {
Template { template }
}
// 変数を置換してレンダリング
fn render(&self, vars: &HashMap<&str, &str>) -> String {
// TODO: 実装
// {{name}} -> vars["name"]
}
// テンプレート内の変数一覧を取得
fn variables(&self) -> Vec<&str> {
// TODO: 実装
}
}
fn main() {
let template = Template::new("Hello, {{name}}! You are {{age}} years old.");
let mut vars = HashMap::new();
vars.insert("name", "Alice");
vars.insert("age", "30");
let result = template.render(&vars);
println!("{}", result);
}
提出物:bonus4_template_engine/プロジェクト
ボーナス5:SQLパーサー(簡易版)(5点)
簡易的なSQLパーサーを実装しなさい。
#[derive(Debug, PartialEq)]
enum SqlStatement<'a> {
Select {
columns: Vec<&'a str>,
table: &'a str,
where_clause: Option<&'a str>,
},
Insert {
table: &'a str,
columns: Vec<&'a str>,
values: Vec<&'a str>,
},
}
fn parse_sql<'a>(sql: &'a str) -> Option<SqlStatement<'a>> {
// TODO: 実装
// サポート:
// SELECT col1, col2 FROM table WHERE condition
// INSERT INTO table (col1, col2) VALUES (val1, val2)
}
fn main() {
let sql = "SELECT name, age FROM users WHERE age > 18";
if let Some(statement) = parse_sql(sql) {
println!("{:?}", statement);
}
}
提出物:bonus5_sql_parser/プロジェクト
---
評価基準
マンダトリー部分(80点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| 問題1:ライフタイム理解 | 20点 | ルールの正確な理解 |
| 問題2:関数実装 | 20点 | 適切なライフタイム注釈 |
| 問題3:構造体実装 | 20点 | 構造体でのライフタイム活用 |
| 問題4:実践的活用 | 20点 | パース処理の実装 |
ボーナス部分(20点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| ボーナス1:設定パーサー | 5点 | 実用性 |
| ボーナス2:トークナイザー | 5点 | 状態管理 |
| ボーナス3:Markdownパーサー | 5点 | 複雑な解析 |
| ボーナス4:テンプレート | 5点 | 置換処理 |
| ボーナス5:SQLパーサー | 5点 | 構文解析 |
注: ボーナスは最大20点まで加算されます。
---
提出方法
ファイル構成
rust-foundations-06/
├── problem1_lifetime_analysis.md
├── problem2_lifetime_functions.rs
├── problem3_lifetime_structs.rs
├── problem4_lifetime_parsing.rs
└── bonus/
├── bonus1_config_parser/
├── bonus2_tokenizer/
├── bonus3_markdown_parser/
├── bonus4_template_engine/
└── bonus5_sql_parser/
提出期限
- マンダトリー:第6章学習後、1週間以内
- ボーナス:第10章修了時まで
---
ヒント
問題2のヒント
longest関数:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
問題3のヒント
first_line実装:
fn first_line(&self) -> &str {
self.text.lines().next().unwrap_or("")
}
問題4のヒント
parse_key_value実装:
fn parse_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
let parts: Vec<&str> = line.split('=').collect();
if parts.len() == 2 {
Some((parts[0], parts[1]))
} else {
None
}
}
---
学習の確認
この課題を通じて、以下を理解できたか確認してください:
- [ ] ライフタイムの概念と必要性
- [ ] ライフタイム注釈の書き方
- [ ] 構造体でのライフタイム使用
- [ ] ライフタイム省略規則
- [ ] 複数のライフタイムパラメータ
- [ ] 'staticライフタイム
- [ ] 参照を返す関数の設計
次の章では、型システムを学びます。Rustの豊富な型システムと型推論について理解を深めます。