mimikakimemo

自分用メモ。

ESP8266 で静電容量式土壌水分センサーを使う

水やりの目安にするため、土壌中の水分量を測定したい。ということで、土壌水分センサーを買ってみた。まずは動かすところから。

土壌水分センサーの種類

手軽に手に入る土壌水分センサーには、電気抵抗式のものと、静電容量式のものがある。

電気抵抗式のセンサーは、2 つの電極が露出している。価格が少し安めだが、使っていると電極が錆びやすいとのこと。

一方、静電容量式のセンサーは電極が露出しておらず、長期間安定して使えるらしい。そんなわけで今回はこちらを選んだ。Amazon だと 5 個セット(そんなに要らないが)で 800 円ほどなので、160 円/個。

Amazon で買ったセンサーの表面には、Capative Soil Moisture Sensor v1.2 と書かれている。調べてみると、DFRobot から販売されている Analog Capacitive Soil Moisture Sensor SEN0193 の、ジェネリック版らしい。

www.dfrobot.com

このセンサーの仕様や使い方については、DFRobot の Wiki に詳しく解説がある。

wiki.dfrobot.com

ESP8266 でアナログ入力を扱う

ESP8266(ESP-WROOM-02)には、アナログ入力に使えるピンが 1 つしかない。TOUT というピン名になっている。Arduino のスケッチ内では、ピン番号として定数 A0 を使う。値の取得には、普通の Arduino 同様、analogRead() 関数を使う1

int value = analogRead(A0);

ひとつ注意が必要なのは、0 V 〜 1 V の範囲でしか入力を受け付けない点。0 V 〜 3.3 V ではない。10 ビットの精度で、0 V のときは 0、1 V のときは 1023 という値が得られる。今回は土壌水分センサーを 3.3 V で動かしているので、手元にあった 10 kΩ の抵抗 3 本を使って、ざっくり 1/3 に分圧した。

f:id:mimikakimemo:20210523020641p:plain

土壌水分量の測定

f:id:mimikakimemo:20210523025240j:plain

analogRead() で読み取った値を、以下のようなコードでそのまま表示してみた。

#include <Arduino.h>

void setup()
{
  Serial.begin(74880);
}

void loop()
{
  const int value = analogRead(A0);
  Serial.println(value);
  delay(1000);
}

条件を変えて簡単に試した結果は以下の通り。

条件 測定値
空気中に出しているとき 394
植木鉢にさしたとき 298
植木鉢に水をやった後 265
水に浸したとき 228

普通の湿度とは逆で、水分が多いほど測定値は小さくなる。このままだとわかりにくいので、空気中における値  V_{\text{air}} を 0%、水中における値  V_{\text{water}} を 100% として、水分量を百分率で表すことにしたい。水分量と測定値の関係が線形だと雑に仮定すると2、測定値が  V のとき、水分量は

 \displaystyle
\frac{V_{\text{air}} - V}
       {V_{\text{air}} - V_{\text{water}}}
\times 100 \,[\%]

で計算できる。

この測定値・水分量は、土の種類や設置位置、センサーの個体差に大きく左右されるはず。しかし、今回の目的は「水やりの目安にするため、湿っているか乾いているかざっくり判定したい」という点なので、あまり細かいことは気にしなくて大丈夫。ただ、こちらの記事にあるとおり、電源電圧の低下には気をつける必要がありそう。

次は水分量を一定間隔で測定して、グラフ化したい。

参考


  1. 昔は analogRead() に対応していなかったようで、古い記事では「system_adc_read() を使う」という記述が見られる。

  2. こちらの記事の測定結果によれば、土壌が乾いてくると線形から外れる様子。

技適対応 Wi-Fi カメラモジュール WT-ESP32-CAM で撮影してみる

ゴーヤのタイムラプス撮影をしようと思い立ち、WT-ESP32-CAM というカメラ付きマイコンモジュールを買ってみた。今回の内容は、以下のブログ記事を参考にしている。

lang-ship.com

製造元 Wireless-Tag 社の公式ページと、データシートは以下のとおり。

ちなみに、Amazon でも ESP32-CAM はいろいろ売っている()が、どれも技適に通っていない。技適対応でタイムラプス用途だと、M5Stack の ESP32 PSRAM Timer Camera X (OV3660) が初心者の自分には扱いやすそうだったが、どこのサイトでも品切れ・入荷未定だったので、WT-ESP32-CAM を使ってみることにした。

購入

AliExpress で 565 円。念のため 2 つ購入し、配送は AliExpress Standard Shipping(507 円)にした。しめて 1,638 円。

www.aliexpress.com

2 週間ほどで到着した。

f:id:mimikakimemo:20210522003710j:plain

組み立て

パッケージを開けると、以下の 3 つが入っている。

  • WT-ESP23-CAM 本体
  • カメラ(OV2640)
  • 外付けアンテナ

f:id:mimikakimemo:20210522003702j:plain

カメラのケーブルを WT-ESP32-CAM 上のコネクタに接続。

付属の外付けアンテナは技適に通っていないので、秋月電子で売っているこちらアンテナの方を使う必要があるらしい。なるほど。

akizukidenshi.com

デモ用のプログラムでカメラ撮影してみる

WT-ESP32-CAM は、デモ用のプログラムが出荷時に書き込み済みなので、電源をつなげるだけでカメラ撮影を試すことができる。USB-シリアル変換モジュールを使って、PC と接続。

f:id:mimikakimemo:20210522003719j:plain

USB-シリアル WT-ESP32-CAM (上の写真では)
5V 5V 紫色
GND GND 青色
TXD U0R 緑色
RXD U0T 灰色

シリアルモニターを 115,200 baud に設定して接続すると、ログを確認することができる。基板左下の RST ボタンを押して再起動。

起動ログ

21:28:15.921 -> ⸮g⸮=⸮ҐL⸮⸮'⸮ҕSH؊⸮њ⸮e⸮    u⸮⸮⸮ʽ⸮⸮J⸮E⸮b⸮⸮:⸮ų³U⸮F⸮*⸮e1⸮
21:28:15.921 -> ⸮=⸮⸮J#⸮⸮⸮V⸮ͩi⸮⸮ *E]⸮:⸮⸮Ս⸮⸮k⸮⸮⸮⸮⸮a0⸮\Eɶ'⸮A⸮    ⸮⸮.⸮⸮⸮⸮,⸮.zѲ⸮¸⸮⸮h⸮%⸮v⸮⸮a⸮i⸮_⸮.⸮⸮⸮⸮
⸮[⸮՚T⸮q⸮,⸮;⸮⸮&k⸮1C!⸮⸮d⸮⸮m⸮⸮a0⸮⸮lY⸮⸮
⸮⸮⸮d⸮⸮mf⸮,⸮ծ⸮⸮#⸮⸮i0⸮⸮0b⸮9K⸮⸮⸮⸮⸮!⸮⸮⸮⸮Q⸮⸮,⸮)K⸮⸮⸮j⸮Y⸮ɹ⸮x⸮⸮8⸮⸮SH⸮⸮a;⸮⸮+A⸮ӊ⸮⸮⸮њ⸮i⸮RնZ⸮⸮⸮⸮̣⸮⸮⸮⸮⸮ړ⸮+A⸮ʀ⸮⸮ѿ⸮⸮⸮⸮⸮ᩁr⸮Wk⸮˂1⸮⸮ž    ⸮⸮⸮⸮Ť9⸮⸮A⸮)Wk⸮⸮K⸮⸮j5
21:28:15.961 -> [0;32mI (38) boot: ESP-IDF v4.0-276-g57a5a486f 2nd stage bootloader[0m
21:28:15.961 -> [0;32mI (38) boot: compile time 18:36:27[0m
21:28:15.961 -> [0;32mI (38) boot: Enabling RNG early entropy source...[0m
21:28:15.961 -> [0;32mI (44) qio_mode: Enabling default flash chip QIO[0m
21:28:15.961 -> [0;32mI (49) boot: SPI Speed      : 80MHz[0m
21:28:16.116 -> [0;32mI (53) boot: SPI Mode       : QIO[0m
21:28:16.116 -> [0;32mI (57) boot: SPI Flash Size : 4MB[0m
21:28:16.116 -> [0;32mI (61) boot: Partition Table:[0m
21:28:16.116 -> [0;32mI (65) boot: ## Label            Usage          Type ST Offset   Length[0m
21:28:16.116 -> [0;32mI (72) boot:  0 factory          factory app      00 00 00010000 003c0000[0m
21:28:16.116 -> [0;32mI (80) boot:  1 nvs              WiFi data        01 02 003d0000 00004000[0m
21:28:16.116 -> [0;32mI (87) boot: End of partition table[0m
21:28:16.116 -> [0;32mI (91) boot_comm: chip revision: 1, min. application chip revision: 0[0m
21:28:16.116 -> [0;32mI (98) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x3010c (196876) map[0m
21:28:16.116 -> [0;32mI (160) esp_image: segment 1: paddr=0x00040134 vaddr=0x3ffb0000 size=0x041a8 ( 16808) load[0m
21:28:16.116 -> [0;32mI (166) esp_image: segment 2: paddr=0x000442e4 vaddr=0x40080000 size=0x00400 (  1024) load[0m
21:28:16.116 -> [0;32mI (167) esp_image: segment 3: paddr=0x000446ec vaddr=0x40080400 size=0x0b924 ( 47396) load[0m
21:28:16.116 -> [0;32mI (191) esp_image: segment 4: paddr=0x00050018 vaddr=0x400d0018 size=0x86610 (550416) map[0m
21:28:16.245 -> [0;32mI (339) esp_image: segment 5: paddr=0x000d6630 vaddr=0x4008bd24 size=0x107e4 ( 67556) load[0m
21:28:16.315 -> [0;32mI (379) boot: Loaded app from partition at offset 0x10000[0m
21:28:16.315 -> [0;32mI (379) boot: Disabling RNG early entropy source...[0m
21:28:16.315 -> [0;32mI (380) psram: This chip is ESP32-D0WD[0m
21:28:16.315 -> [0;32mI (384) spiram: Found 64MBit SPI RAM device[0m
21:28:16.315 -> [0;32mI (389) spiram: SPI RAM mode: flash 80m sram 40m[0m
21:28:16.315 -> [0;32mI (394) spiram: PSRAM initialized, cache is in low/high (2-core) mode.[0m
21:28:16.315 -> [0;32mI (401) cpu_start: Pro cpu up.[0m
21:28:16.365 -> [0;32mI (405) cpu_start: Application information:[0m
21:28:16.365 -> [0;32mI (410) cpu_start: Project name:     camera_web_server[0m
21:28:16.365 -> [0;32mI (416) cpu_start: App version:      2470e47[0m
21:28:16.365 -> [0;32mI (421) cpu_start: Compile time:     Jun 23 2020 18:36:23[0m
21:28:16.365 -> [0;32mI (427) cpu_start: ELF file SHA256:  657db6d4b907e61e...[0m
21:28:16.365 -> [0;32mI (433) cpu_start: ESP-IDF:          v4.0-276-g57a5a486f[0m
21:28:16.365 -> [0;32mI (439) cpu_start: Starting app cpu, entry point is 0x400814e4[0m
21:28:16.365 -> [0;32mI (0) cpu_start: App cpu up.[0m
21:28:17.238 -> [0;32mI (1329) spiram: SPI SRAM memory test OK[0m
21:28:17.238 -> [0;32mI (1329) heap_init: Initializing. RAM available for dynamic allocation:[0m
21:28:17.272 -> [0;32mI (1330) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM[0m
21:28:17.272 -> [0;32mI (1336) heap_init: At 3FFBD660 len 000229A0 (138 KiB): DRAM[0m
21:28:17.272 -> [0;32mI (1342) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM[0m
21:28:17.272 -> [0;32mI (1349) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM[0m
21:28:17.272 -> [0;32mI (1355) heap_init: At 4009C508 len 00003AF8 (14 KiB): IRAM[0m
21:28:17.272 -> [0;32mI (1361) cpu_start: Pro cpu start user code[0m
21:28:17.321 -> [0;32mI (1366) spiram: Adding pool of 4096K of external SPI memory to heap allocator[0m
21:28:17.321 -> [0;32mI (1387) spi_flash: detected chip: generic[0m
21:28:17.321 -> [0;32mI (1387) spi_flash: flash io: qio[0m
21:28:17.321 -> [0;32mI (1387) cpu_start: Starting scheduler on PRO CPU.[0m
21:28:17.321 -> [0;32mI (0) cpu_start: Starting scheduler on APP CPU.[0m
21:28:17.321 -> [0;32mI (1396) spiram: Reserving pool of 32K of internal memory for DMA/internal allocations[0m
21:28:17.360 -> I (1446) wifi:wifi driver task: 3ffcceb0, prio:23, stack:3584, core=0
21:28:17.360 -> [0;32mI (1446) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE[0m
21:28:17.360 -> [0;32mI (1446) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE[0m
21:28:17.407 -> I (1466) wifi:wifi firmware version: 101cee8
21:28:17.407 -> I (1466) wifi:config NVS flash: enabled
21:28:17.407 -> I (1466) wifi:config nano formating: disabled
21:28:17.407 -> I (1466) wifi:Init dynamic tx buffer num: 32
21:28:17.407 -> I (1476) wifi:Init data frame dynamic rx buffer num: 32
21:28:17.407 -> I (1476) wifi:Init management frame dynamic rx buffer num: 32
21:28:17.449 -> I (1486) wifi:Init management short buffer num: 32
21:28:17.449 -> I (1486) wifi:Init static tx buffer num: 16
21:28:17.449 -> I (1496) wifi:Init static rx buffer size: 1600
21:28:17.449 -> I (1496) wifi:Init static rx buffer num: 10
21:28:17.449 -> I (1496) wifi:Init dynamic rx buffer num: 32
21:28:17.449 -> [0;32mI (1506) camera wifi: wifi_init_softap finished.SSID:wireless-tag password:wireless-tag[0m
21:28:17.521 -> [0;32mI (1596) phy: phy_version: 4180, cb3948e, Sep 12 2019, 16:39:13, 0, 0[0m
21:28:17.521 -> I (1596) wifi:mode : softAP (30:ae:a4:96:af:75)
21:28:17.521 -> I (1606) wifi:Total power save buffer number: 8
21:28:17.521 -> I (1606) wifi:Init max length of beacon: 752/752
21:28:17.521 -> I (1606) wifi:Init max length of beacon: 752/752
21:28:17.556 -> I (1616) wifi:Set ps type: 0
21:28:17.556 -> 
21:28:17.556 -> [0;32mI (1616) sccb: pin_sda 26 pin_scl 27
21:28:17.556 -> [0m
21:28:17.556 -> [0;32mI (1616) gpio: GPIO[32]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 [0m
21:28:17.665 -> [0;32mI (1746) camera: Detected OV2640 camera[0m
21:28:17.665 -> [0;32mI (1756) gpio: GPIO[35]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.699 -> [0;32mI (1756) gpio: GPIO[34]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.699 -> [0;32mI (1766) gpio: GPIO[39]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.699 -> [0;32mI (1776) gpio: GPIO[36]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.699 -> [0;32mI (1786) gpio: GPIO[21]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.734 -> [0;32mI (1786) gpio: GPIO[19]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.734 -> [0;32mI (1796) gpio: GPIO[18]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.734 -> [0;32mI (1806) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.734 -> [0;32mI (1816) gpio: GPIO[25]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.772 -> [0;32mI (1826) gpio: GPIO[23]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.772 -> [0;32mI (1836) gpio: GPIO[22]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 [0m
21:28:17.772 -> [0;32mI (1846) camera: Allocating 2 frame buffers (750 KB total)[0m
21:28:17.807 -> [0;32mI (1906) camera: Allocating 375 KB frame buffer in OnBoard RAM[0m
21:28:17.906 -> [0;32mI (2006) camera: Allocating 375 KB frame buffer in OnBoard RAM[0m
21:28:18.271 -> [0;32mI (2356) camera_httpd: Starting web server on port: '80'[0m
21:28:18.271 -> [0;32mI (2356) camera_httpd: Starting stream server on port: '81'[0m
21:28:18.271 -> [0;32mI (2356) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE[0m

このログの中に

21:28:17.449 -> [0;32mI (1506) camera wifi: wifi_init_softap finished.SSID:wireless-tag password:wireless-tag[0m
21:28:17.521 -> [0;32mI (1596) phy: phy_version: 4180, cb3948e, Sep 12 2019, 16:39:13, 0, 0[0m
21:28:17.521 -> I (1596) wifi:mode : softAP (30:ae:a4:96:af:75)

とあるとおり、SoftAP モードで動いていて WT-ESP32-CAM 自身が Wi-Fi アクセスポイントの役割もしているので、スマホや PC から

  • SSID: wireless-tag
  • パスワード: wireless-tag

で接続することができる。 WT-ESP32-CAM に外部アンテナを付けていない状態でも、近距離であれば問題なく接続できた。

このアクセスポイントに接続し、ブラウザで http://192.168.4.1/ にアクセスすると、カメラ撮影用の Web インターフェイスを使うことができる。[Get Still] ボタンをクリックすると静止画が撮影される。

f:id:mimikakimemo:20210522002826p:plain

最大で 1600 px × 1200 px の画像が撮影できる。小さくて安いのにすごい。

トラブルシューティング

起動時、最初は

21:26:03.260 -> [0;31mE (2056) camera: Failed to set frame size[0m
21:26:03.260 -> [0;31mE (2066) camera: Camera init failed with error 0x20002[0m
21:26:03.260 -> [0;31mE (2066) app_camera: Camera init failed with error 0x20002[0m

というカメラ関連のエラーが出たが、何回かリセットボタンで再起動したら直った。

ちなみに、カメラが接続されていない場合には、以下のような別のエラーが出る。

21:14:10.916 -> [0;31mE (1686) camera: Detected camera not supported.[0m
21:14:10.916 -> [0;31mE (1686) camera: Camera probe failed with error 0x20004[0m
21:14:10.916 -> [0;31mE (1686) app_camera: Camera init failed with error 0x20004[0m

また、1台の WT-ESP32-CAM に、複数台のクライアントから同時にアクセスすることはできない様子。

21:31:16.681 -> I (180716) wifi:max connection, deauth!

Rust に付け焼き刃で入門する 5

Rust に付け焼き刃で入門する 4 の続き。

6. Enum とパターンマッチング

6.1. Enumを定義する

Enumを定義する - The Rust Programming Language 日本語版

enum IpAddrKind {
    V4,
    V6,
}

enum の定義。V4V6 を日本語ドキュメントでは列挙子と呼んでいるが、英語だと単純に variant。列挙子の名前は識別子として扱われているっぽいので、記号やスペースは含められないんだろうか? そもそも、Rust の識別子に使える文字種がどうなっているのか、まだ知らないが。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

列挙子のインスタンスを生成する。IpAddrKind::V4IpAddrKind::V6 は値。その両方を含んだ型が IpAddrKind

:: を使っているのは、関連関数の :: と関係あるんだろうか?

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

このようにして、列挙子に具体的なデータを格納することができる。列挙型はユニオン型のようになる。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Move は匿名構造体をデータとして持つ列挙子。匿名構造体ってのは初めて出てきた気がする。他の箇所でも使えるんだろうか。

enumと構造体にはもう1点似通っているところがあります: implを使って構造体にメソッドを定義できるのと全く同様に、 enumにもメソッドを定義することができるのです。

なるほど。関連関数は定義できるんだろうか?と思って試してみたらいけた。

#[derive(Debug)]
enum Hoge {
    Foo(i32),
    Bar(bool),
}

impl Hoge {
    fn print(&self) {
        println!("{:?}", self);
    }
    // 関連関数を定義
    fn make_bar(value: bool) -> Hoge {
        Hoge::Bar(value)
    }
}

fn main() {
    let foo = Hoge::Foo(123);
    let bar = Hoge::make_bar(true); // 関連関数を呼び出す
    foo.print(); // → Foo(123)
    bar.print(); // → Bar(true)
}

Rustにはnullがありませんが、 値が存在するか不在かという概念をコード化するenumならあります。このenumOption<T>で、 以下のように標準ライブラリに定義されています。

Haskell でいう Maybe 的なやつ。

Option<T>は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。 さらに、列挙子もそうなっています: SomeNoneOption::の接頭辞なしに直接使えるわけです。

しれっと使われている SomeNone は、Option::SomeOption::Noneenum だったのか。

では、Option<T>型の値がある時、その値を使えるようにするには、どのようにSome列挙子からT型の値を取り出せばいいのでしょうか? Option<T>には様々な場面で有効に活用できる非常に多くのメソッドが用意されています; ドキュメントでそれらを確認できます。

Some(42) から値 42 を取り出す方法は、この節では濁されている。ドキュメントを見てみると、unwrap() メソッドでできるようだ。

6.2. matchフロー制御演算子

matchフロー制御演算子 - The Rust Programming Language 日本語版

パターンマッチ。説明されていないが、各アームが返す型はすべて同じでなければならないようだ。

Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。

これも Haskell など他の言語と同様。また、ワイルドカード的なプレースホルダとして、パターン中に _ が使える。

6.3. if letで簡潔なフロー制御

if letで簡潔なフロー制御 - The Rust Programming Language 日本語版

let some_u8_value = Some(0u8);

match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

let some_u8_value = Some(0u8);

if let Some(3) = some_u8_value {
    println!("three");
}

は等価。if let と書かれていたので一瞬構造がよくわからなかったが、「普通の if 式の条件の部分に let Some(3) = some_u8_value が入っている」と見ればよさそう。変数定義に使う let と被っているのも紛らわしいが、パターンマッチのための「let <pattern> = <valiable>」という特別な構文、という理解でいいんだろうか。


enum とパターンマッチ。若干独特な部分はあるが、変な概念はない。

Rust に付け焼き刃で入門する 4

Rust に付け焼き刃で入門する 3 の続き。

5. 構造体を使用して関係のあるデータを構造化する

5.1. 構造体を定義し、インスタンス化する

構造体を定義し、インスタンス化する - The Rust Programming Language 日本語版

フィールドには foo.bar のようにドットでアクセス。

Rustでは、一部のフィールドのみを可変にすることはできないのです。

フィールドの値を書き換えたい場合は、構造体のインスタンス全体を可変にする。今まで見てきた変数と同じように、let mut を使う。

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

フィールド初期化省略記法

fn build_user(email: String, username: String) -> User {
    User {
        email, // ← これ
        username, // ← これ
        active: true,
        sign_in_count: 1,
    }
}

field init shorthand syntax。JavaScript にもあるやつ。

構造体更新記法

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

struct update syntax。JavaScript の spreading に近いが、..foo は必ず最後に書く必要がある様子。

タプル構造体

struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

型に名前をつけて区別できること以外は、普通のタプルと同じ。要素に red, green, blue など名前をつけたくなるが、それはできないようだ。

普通の構造体の定義は ; が不要だが、タプル構造体の定義は ; が必要。若干ややこしいが、VS Code の拡張を入れておけば、保存時のフォーマットでよしなに直してくれる。

ユニット様構造体

struct Foo; だけで定義できるらしい。

5.2. 構造体を使ったプログラム例

構造体を使ったプログラム例 - The Rust Programming Language 日本語版

構造体を使ったかんたんなチュートリアル。構造体の定義の直前に #[derive(Debug)] をつけると、println! 文字列に {:?} を書いていい感じに出力できるようになる。derive マクロというやつなんだろうか。

5.3 メソッド記法

メソッド記法 - The Rust Programming Language 日本語版

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl ブロックによって、構造体にメソッドを定義できる。fn といい mut といい struct といい、Rust のキーワードは省略形が多い。

第1引数 &self で自分自身のインスタンスを受け取るのは Python っぽい。&self には型注釈は不要。試してみたら、一応 self: &Rectangle と書くことはできるようだ。& が頭につくことに注意。可変にするなら &mut self

selfだけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、 メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。

そうなのか。

Rustには->演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。

自動でよしなにしてくれて若干気持ち悪いが、C の .-> の使い分けもそれはそれで面倒なので、まあ。

メソッドの受け手に関して借用が明示されないというのが、 所有権を実際に使うのがRustにおいて簡単である大きな理由です。

意味がとりづらいが、「メソッドの第1引数(&self&mut selfself)を見るだけで、メソッド呼び出しによって借用が起きるかどうかをコンパイラが判断できる。なので、メソッドの受け手 reciever(そのインスタンス自体。foo.do_something()foo)に &&mut をつける必要がない(&foo.do_something()foo.do_something() か、というようなことに人間が注意しなくても良い)」ということだろうか。

implブロック内にselfを引数に取らない関数を定義できることです。 これは、構造体に関連付けられているので、関連関数と呼ばれます。

associated function。いわゆる静的メソッド。some_instance.do_something() ではなく、SomeStruct::do_something() で呼び出す。

複数のimplブロックが有用になるケースは第10章で見ますが、そこではジェネリック型と、トレイトについて議論します。

まだよくわからない。構造体自体の定義 struct とメソッド定義 impl が文法上別々のブロックに分かれているのは、これを可能にするためなんだろうか?


構造体おわり。特にはまりそうなところは無かった。

Rust に付け焼き刃で入門する 3

Rust に付け焼き刃で入門する 2 の続き。

4. 所有権を理解する

Rust 独特の機能。「Rust を手っ取り早く解説」系の記事では、あまりよく挙動がわからなかったので、しっかり理解したい。

4.1. 所有権とは

所有権とは? - The Rust Programming Language 日本語版

プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。

GC でも malloc/free でもない第3の方法が所有権 ownership システム。

この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。

  • スタック:高速。LIFO。既知で固定サイズのデータに限る
  • ヒープ:低速。使いたいときは領域を確保 allocate して、そのポインタを返す

いきなり結構低レベルな話になってきた。このあたりの概念は Rust に特有ということでもない。

コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。

ここも一般的なコールスタックの話。

一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由

なるほど。

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。

このルールは明快なので、「手っ取り早く解説」系の記事でも読んで覚えている。

変数は、宣言された地点から、現在のスコープの終わりまで有効になります。

よくある感じ。スコープが何を指すかの説明がないが、とりあえずは { ... } を思い描いておけばいいんだろうか。

Rustには、 2種類目の文字列型、String型があります。この型はヒープにメモリを確保するので、 コンパイル時にはサイズが不明なテキストも保持することができるのです。

ちなみに、もう一方の型(文字列リテラルの型)は &str 型のようだ。両者の名前が似ていて最初は???という感じだったが、よくある区別。

変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。[…] Rustは、閉じ波括弧で自動的にdrop関数を呼び出します。

デストラクタ的な。

他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、 データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。 ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、 ムーブとして知られているわけです。

shallow copy だけだと二重解放の問題が起きるので、shallow copy っぽいことをしつつ、古い変数の方は無効にする。これがムーブ。なるほど、そういうことか。

ムーブによって「いかなる時も所有者は一つである」というルールが満たされる。すると、後は「所有者がスコープから外れたら、値は破棄される」というルールだけ用意しておけば、二重解放や、解放し忘れに悩まされることがなくなる。

Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。

ふむ。自動コピー automatic copy ってのは後で出てくるのかな。

型がCopyトレイトに適合していれば、代入後も古い変数が使用可能になります。

ルールはそんなに単純じゃなかった。他の言語も deep copyshallow copy が混ざったりするので、それと同じだといえば同じだが。プリミティブだけでなく、例えば (i32, i32)Copy らしい。

関数に変数を渡すと、 代入のようにムーブやコピーされます。

関数に変数を渡すと関数にムーブされる、というのはなんか独特な感じ。関数にムーブされた変数は、その関数を抜けるときに drop される。変数が Copy だったら、関数に渡すとムーブではなくコピーされる。参照渡しと値渡しのようなものではあるが…。

関数から値を返した場合は、drop されずに呼び出し元(の新たな変数)にムーブされる。

4.2. 参照と借用

参照と借用 - The Rust Programming Language 日本語版

fn main() {
    let s1 = String::from("hello");
    // s1 を渡すと calculate_length にムーブしてしまうが、次の println! でもう一度使いたいので、関数から返してもらう必要がある
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

受け取った変数をいちいち返すのは煩雑なので、参照 reference という仕組みがある。

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

変数名や型注釈に & をつける。C言語っぽい記法(忘れていたが、C の & はアドレス演算子という名前らしい)。

この&s1という記法により、s1の値を参照する参照を生成することができますが、これを所有することはありません。所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。

意味の取りづらい日本語だが、原文を見てみるに、「&s1 って書くと s1 の参照を作ることができるけど、参照 &s1 は変数 s1 を所有するわけじゃないよ。参照 &s1 があるスコープを抜けたとしても、&s1 が参照している変数 s1 はドロップされないよ。&s1s1 を所有していないからね。」ということらしい。

関数の引数に参照を取ることを借用と呼びます。

borrowing。

変数が標準で不変なのと全く同様に、参照も不変なのです。

&foo は不変。&mut foo で渡せば、可変になる。

ところが、可変な参照には大きな制約が一つあります: 特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。

さらに不変な参照をしている間は、可変な参照をすることはできません。

競合を防ぐため。納得できる。一方で、不変な参照だけであれば、いくつも同時に作ることができる。

対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます

浮いた参照にならないように、コンパイルエラーにしてくれる。

4.3. スライス型

スライス型 - The Rust Programming Language 日本語版

所有権のない別のデータ型は、スライスです。

なるほど? 「別の」ということは、参照とも違う扱いなんだろうか。Python のスライスみたいなものかな?

for (i, &item) in bytes.iter().enumerate() {

このへんも Python っぽい。

let s = String::from("hello");
let slice = &s[3..]; // "lo"

sString 型のとき、文字列スライス &s[3..]&str 型。スライスとして表現することによって、元の変数に対する変更などをコンパイラでチェックすることができる。

let s = "Hello, world!";

ここでのsの型は、&strです: バイナリのその特定の位置を指すスライスです。 これは、文字列が不変である理由にもなっています。要するに、&strは不変な参照なのです。

な、なるほど…。そういう扱いなのか。


所有権まわりのメリットはだいぶ理解できた。