課題4: 所有権システムの理解
マンダトリー要件
問題1:所有権の基本理解(25点)
以下のコードについて、コンパイルエラーが発生するか判定し、理由を説明しなさい。
// 1-1
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
}
// 1-2
fn main() {
let x = 5;
let y = x;
println!("{}, {}", x, y);
}
// 1-3
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
}
// 1-4
fn main() {
let s = String::from("hello");
takes_ownership(s);
println!("{}", s);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
// 1-5
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}, {}", s1, s3);
}
fn gives_ownership() -> String {
String::from("yours")
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
各コードについて:
- コンパイルできるか?(できる/できない)
- 理由を所有権の観点から説明
- エラーがある場合、どう修正するか(3通り示す)
提出物:problem1_analysis.md
問題2:所有権を考慮した関数実装(25点)
以下の関数を所有権のルールに従って実装しなさい。
/// 文字列の長さを返す(所有権を渡して返す)
fn string_length_move(s: String) -> (String, usize) {
// TODO: 実装
// sの長さを計算し、sと長さのタプルを返す
}
/// 文字列に接尾辞を追加して返す
fn append_suffix(mut s: String, suffix: &str) -> String {
// TODO: 実装
// sにsuffixを追加してsを返す
}
/// 2つの文字列を結合して新しい文字列を返す(両方ムーブ)
fn concatenate(s1: String, s2: String) -> String {
// TODO: 実装
}
/// 整数の配列の所有権を受け取り、合計を返す
fn sum_array(arr: Vec<i32>) -> (Vec<i32>, i32) {
// TODO: 実装
// 配列の合計を計算し、配列と合計のタプルを返す
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_length_move() {
let s = String::from("hello");
let (s, len) = string_length_move(s);
assert_eq!(len, 5);
assert_eq!(s, "hello");
}
#[test]
fn test_append_suffix() {
let s = String::from("hello");
let result = append_suffix(s, " world");
assert_eq!(result, "hello world");
}
#[test]
fn test_concatenate() {
let s1 = String::from("hello");
let s2 = String::from(" world");
let result = concatenate(s1, s2);
assert_eq!(result, "hello world");
}
#[test]
fn test_sum_array() {
let arr = vec![1, 2, 3, 4, 5];
let (arr, sum) = sum_array(arr);
assert_eq!(sum, 15);
assert_eq!(arr, vec![1, 2, 3, 4, 5]);
}
}
提出物:problem2_ownership.rs
問題3:ムーブとコピーの実践(25点)
以下の要件を満たすプログラムを実装しなさい。
3-1: ポイント構造体
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
/// 新しいPointを作成
fn new(x: i32, y: i32) -> Point {
// TODO: 実装
}
/// 原点からの距離を計算(所有権を消費)
fn distance_from_origin(self) -> f64 {
// TODO: 実装
// ヒント:(x² + y²)の平方根
}
/// 別のPointまでの距離を計算(両方消費)
fn distance_to(self, other: Point) -> f64 {
// TODO: 実装
}
}
fn main() {
let p1 = Point::new(3, 4);
let distance = p1.distance_from_origin();
println!("距離: {}", distance);
let p2 = Point::new(0, 0);
let p3 = Point::new(3, 4);
let distance = p2.distance_to(p3);
println!("距離: {}", distance);
}
3-2: Copyトレイトの追加
上記のPoint構造体にCopyトレイトを追加し、動作を比較しなさい。
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
// メソッドは同じだが、selfではなく&selfに変更
impl Point {
fn distance_from_origin(&self) -> f64 {
// TODO: 実装
}
}
fn main() {
let p1 = Point::new(3, 4);
let distance = p1.distance_from_origin();
println!("p1: {:?}, 距離: {}", p1, distance); // p1はまだ使える!
}
提出物:
problem3_point_move.rs: ムーブ版problem3_point_copy.rs: Copy版comparison.md: 2つの違いの説明
問題4:複雑な所有権パターン(25点)
実用的なデータ構造を実装しなさい。
4-1: スタック構造
struct Stack {
items: Vec<String>,
}
impl Stack {
/// 新しい空のスタックを作成
fn new() -> Stack {
// TODO: 実装
}
/// 要素をプッシュ(所有権を受け取る)
fn push(&mut self, item: String) {
// TODO: 実装
}
/// 要素をポップ(所有権を返す)
fn pop(&mut self) -> Option<String> {
// TODO: 実装
}
/// スタックが空か確認
fn is_empty(&self) -> bool {
// TODO: 実装
}
/// スタックのサイズ
fn size(&self) -> usize {
// TODO: 実装
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stack() {
let mut stack = Stack::new();
assert!(stack.is_empty());
stack.push(String::from("first"));
stack.push(String::from("second"));
assert_eq!(stack.size(), 2);
assert_eq!(stack.pop(), Some(String::from("second")));
assert_eq!(stack.pop(), Some(String::from("first")));
assert_eq!(stack.pop(), None);
}
}
4-2: シンプルなメッセージキュー
struct MessageQueue {
messages: Vec<String>,
}
impl MessageQueue {
fn new() -> MessageQueue {
// TODO: 実装
}
/// メッセージを送信(enqueue)
fn send(&mut self, message: String) {
// TODO: 実装
}
/// メッセージを受信(dequeue)
fn receive(&mut self) -> Option<String> {
// TODO: 実装
// ヒント:Vec::remove(0)を使用
}
/// キュー内のメッセージ数
fn count(&self) -> usize {
// TODO: 実装
}
}
fn main() {
let mut queue = MessageQueue::new();
queue.send(String::from("Hello"));
queue.send(String::from("World"));
while let Some(msg) = queue.receive() {
println!("受信: {}", msg);
}
}
提出物:problem4_data_structures.rs
---
ボーナス課題
> ボーナス: 以下はオプションです。マンダトリー要件を完了してから挑戦してください。
ボーナス1:所有権の可視化ツール(10点)
所有権の動きを可視化するデバッグツールを作成しなさい。
#[derive(Debug)]
struct TrackedString {
value: String,
id: usize,
}
static mut COUNTER: usize = 0;
impl TrackedString {
fn new(s: &str) -> TrackedString {
unsafe {
COUNTER += 1;
let id = COUNTER;
println!("[Create {}] '{}'", id, s);
TrackedString {
value: String::from(s),
id,
}
}
}
}
impl Drop for TrackedString {
fn drop(&mut self) {
println!("[Drop {}] '{}'", self.id, self.value);
}
}
impl Clone for TrackedString {
fn clone(&self) -> TrackedString {
unsafe {
COUNTER += 1;
let id = COUNTER;
println!("[Clone {} -> {}] '{}'", self.id, id, self.value);
TrackedString {
value: self.value.clone(),
id,
}
}
}
}
fn main() {
println!("=== テストプログラム ===");
let s1 = TrackedString::new("hello");
println!("s1作成完了");
let s2 = s1;
println!("s1からs2へムーブ");
let s3 = s2.clone();
println!("s2をクローンしてs3作成");
println!("=== 終了 ===");
}
拡張要件:
- ムーブの追跡
- スコープの可視化
- メモリ使用量の表示
提出物:bonus1_tracker/プロジェクト
ボーナス2:所有権パズルゲーム(10点)
所有権の概念を学ぶためのパズルゲームを作成しなさい。
ゲーム概要:
レベル1:基本的なムーブ
┌─────────────────┐
│ let x = "data" │
│ let y = x │
│ │
│ 質問:xは使える?│
│ A) はい B) いいえ│
└─────────────────┘
レベル2:Copy vs Move
レベル3:関数呼び出し
レベル4:複雑なパターン
提出物:bonus2_puzzle_game/プロジェクト
ボーナス3:カスタムスマートポインタ(10点)
独自のスマートポインタを実装しなさい。
struct MyBox<T> {
value: T,
}
impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
// TODO: 実装
}
fn into_inner(self) -> T {
// TODO: 実装
// 所有権を返す
}
}
// Derefトレイトの実装
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
// TODO: 実装
}
}
fn main() {
let boxed = MyBox::new(String::from("hello"));
println!("{}", *boxed); // Derefで参照解決
let value = boxed.into_inner();
println!("{}", value);
}
提出物:bonus3_smart_pointer.rs
ボーナス4:メモリリーク検出ツール(10点)
循環参照などのメモリリークを検出するツールを作成しなさい。
要件:
- Rc
の使用を検出 - 参照カウントの追跡
- 循環参照の警告
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
fn detect_cycle(node: &Rc<RefCell<Node>>) -> bool {
// TODO: 実装
// 循環参照を検出
}
fn main() {
// 循環参照を作成
let node1 = Rc::new(RefCell::new(Node {
value: 1,
next: None,
}));
let node2 = Rc::new(RefCell::new(Node {
value: 2,
next: Some(Rc::clone(&node1)),
}));
node1.borrow_mut().next = Some(Rc::clone(&node2));
if detect_cycle(&node1) {
println!("警告:循環参照を検出しました!");
}
}
提出物:bonus4_leak_detector/プロジェクト
ボーナス5:所有権ベンチマーク(10点)
ムーブ、クローン、参照の性能を比較しなさい。
use std::time::Instant;
fn benchmark_move() {
let start = Instant::now();
for _ in 0..1_000_000 {
let s = String::from("hello");
let _s2 = s; // ムーブ
}
let duration = start.elapsed();
println!("ムーブ: {:?}", duration);
}
fn benchmark_clone() {
let start = Instant::now();
let s = String::from("hello");
for _ in 0..1_000_000 {
let _s2 = s.clone(); // クローン
}
let duration = start.elapsed();
println!("クローン: {:?}", duration);
}
fn benchmark_reference() {
let start = Instant::now();
let s = String::from("hello");
for _ in 0..1_000_000 {
let _s2 = &s; // 参照(次の章で学習)
}
let duration = start.elapsed();
println!("参照: {:?}", duration);
}
fn main() {
benchmark_move();
benchmark_clone();
benchmark_reference();
}
拡張要件:
- 様々なサイズのデータで測定
- メモリ使用量の測定
- グラフ化
提出物:
bonus5_benchmark/プロジェクトbenchmark_report.md: 結果分析
---
評価基準
マンダトリー部分(80点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| 問題1:基本理解 | 25点 | 所有権ルールの正確な理解 |
| 問題2:関数実装 | 25点 | 所有権を考慮した設計 |
| 問題3:ムーブとコピー | 25点 | Copy/Cloneの適切な使用 |
| 問題4:データ構造 | 25点 | 実践的な所有権管理 |
ボーナス部分(20点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| ボーナス1:可視化 | 10点 | 教育的な価値 |
| ボーナス2:ゲーム | 10点 | 学習ツールとしての質 |
| ボーナス3:スマートポインタ | 10点 | 所有権の深い理解 |
| ボーナス4:リーク検出 | 10点 | 実用性 |
| ボーナス5:ベンチマーク | 10点 | 性能分析の質 |
注: ボーナスは最大20点まで加算されます。
---
提出方法
ファイル構成
rust-foundations-04/
├── problem1_analysis.md
├── problem2_ownership.rs
├── problem3_point_move.rs
├── problem3_point_copy.rs
├── comparison.md
├── problem4_data_structures.rs
└── bonus/
├── bonus1_tracker/
├── bonus2_puzzle_game/
├── bonus3_smart_pointer.rs
├── bonus4_leak_detector/
└── bonus5_benchmark/
提出期限
---
ヒント
問題2のヒント
タプルでの所有権の返却:
fn string_length_move(s: String) -> (String, usize) {
let len = s.len();
(s, len) // タプルで両方返す
}
問題3のヒント
Copyトレイトの条件:
- すべてのフィールドがCopyを実装している
- ヒープメモリを使用していない
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32, // i32はCopy
y: i32, // i32はCopy
// すべてCopy → Point全体がCopyになれる
}
問題4のヒント
Vecの先頭要素を削除:
let first = vec.remove(0); // インデックス0を削除して返す
ボーナス5のヒント
criterion crateでより正確なベンチマーク:
[dev-dependencies]
criterion = "0.5"
---
学習の確認
この課題を通じて、以下を理解できたか確認してください:
- [ ] 所有権の3つのルール
- [ ] ムーブセマンティクス
- [ ] Copyトレイトの条件
- [ ] cloneの使いどころ
- [ ] 関数と所有権の関係
- [ ] スコープとドロップ
- [ ] 所有権を考慮したデータ構造設計
次の章では、所有権を移動せずに値を使う方法として参照と借用を学びます。