sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

VisualStudio デフォルトエンコーディングの指定

VisualStudio のデフォルトエンコーディングは、隙あらば shift-jis が使われました。これまでは。

これとにかく気持ち悪くて、shift-jis と UTF8 が混在するのは VisualStudio のコーディングする上で気づいたらそうなってしまうもので、vscode と違ってパッと見てエンコーディングもわからないので、不満でした。

日本語が混在すると勝手に切り替わる仕様があった?(参考より)

設定変更

  • ツール>オプション>環境>ドキュメント

設定前:

設定後:

特定のエンコーディングでファイルを保存を変更しましょう。

おまけ

日本語の windows 環境では shift-jis が今でもよく使われるので UTF-8 と shift-jis を区分するために BOM が有効だ、という指摘があります。shift-jis 駆逐したい私は賛成派。

BOM は EF BB BF というバイト列をファイルの頭に付与する仕組みです。ちなみに、Unicode 標準では、UTF-8 でエンコードされたストリームに BOM を使用は推奨されていないみたいです。Visual Studio の日本語の都合です。(補足すると UTF-16/32 のために示すものだから)

var fs = new FileStream("bom.txt", FileMode.Open);
var bits = new byte[3];

fs.Read(bits, 0, 3);

// BOM の判定例
if (bits[0] == 0xEF && bits[1] == 0xBB && bits[2] == 0xBF)
{
    ...
}

簡単な仕様ですが BOM はデメリットもあって、プログラムが間違って BOM なし UTF8 のファイルだと解釈すると BOM の部分がテキスト中に含まれることで、誤動作に繋がる恐れがあります。見えない文字なので、文字化けよりも気づきづらい恐れもある点は留意してよいと思います。

Visual Studio は公式に BOM を期待する設定になっているので使うことになりますが、Linux や Web 系では一般的に使うべきではないと思います。(使われてもいない)UTF8 with BOM は、ここも日本語の Windows 環境の独自性からメリットが発生している都合の認識です。

テキストエディタによっては、BOM をつけるかつけないかを指定できることもあるので、用途を知っておくと GOOD ではないかと。

参考

WPF Calendar コントロールのカスタマイズ

WPF のカレンダーコントロールをしっかりとやっている記事が少なかったので記録を残します。Calendar コントロールは、正直な話をするとあまりよく出来た使いやすいコントロールだという印象がありません。どちらかと言えば、カスタマイズする上でも使いづらいように個人的には思っています。

作成したカレンダー

しかし、デフォルトで提供されているカレンダーはこれくらいなので、結局付き合うハメになる。

カレンダーをカスタマイズするうえでよい見本は、つぎの Youtube がベストだったと思います。

WPF C# | How to customize Calendar Control in WPF

というよりも、複雑なコントールをカスタマイズするときは、この動画のように Visual Studio の機能を使ってテンプレートの基礎になるコードを生成しないと、とっかかりに困ると思う。

xaml ファイルのデザイン画面から右クリックで「テンプレートの編集」、さらに「追加テンプレートの編集」でスタイルとテンプレートを生成できることは覚えておいたほうがいいと思いますので必見。

今回のように、コントールの外観を再設計する場合なんかに思い出せるようにしておく。

カレンダーコントロールの構造

WPF の Calendar コントロールは、つぎの3つのコントロール、つまり、「日(縦6x横7)の42日間の表示」と、「月(縦3x横4)12か月の表示」と、「年(縦3x横4)の12年の表示」の3パターンの表示があります。(違ったらすいません)

コントール的には:

  • PART_Root
    • PART_PreviousButton
    • PART_HeaderButton
    • PART_NextButton
    • 日(※ Name は無いはず)
    • 月 PART_MonthView
    • 年 PART_YearView

カレンダー表示は、3つのコントロール(Disabled 用の PART_DisabledVisual もあるけど、ここでは割愛)からどれか1つを DisplayMode プロパティで選択し、選択されたコントロールを表示し、非選択コントロールを非表示にすることで表示切替を実装しているのだけど、非表示の状態でもコントロールの大きさは確保されたままになっている。Visiblity.Hidden みたいなものです。

この仕様でコントロールをカスタマイズする上で注意が必要になるのは、カレンダーのセル数と各セルの大きさ。日の表示をするときは(6x7)だが、年/月の表示をするときは(3x4)になっている。

たとえば、日のセルサイズを 120px に設定すると横幅は 120x7=840px になる。なので、年/月のセルサイズは 840/4=210px にしておかないと、変な余白が残るコントロールになってしまう。

<Style x:Key="CalendarCalendarDayButtonStyle" TargetType="{x:Type CalendarDayButton}">
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="120"/>
</Style>

<Style x:Key="CalendarCalendarButtonStyle" TargetType="{x:Type CalendarButton}">
    <!-- 年、月のセルサイズ 120px*7cell=840px / 4cell = 210px -->
    <Setter Property="MinWidth" Value="210"/>
    <Setter Property="MinHeight" Value="210"/>
</Style>

Day セルのカスタマイズ

日のセルにイベントを追加して、セルになにか表示したいときの一例。通常の「日」のセル表示の部分の xaml だけを取り出すとこんな感じに表すことができると思います。

UserControl を CustomCalendar として定義したとします。

<UserControl>
    <Grid>
        <Viewbox Stretch="Uniform">
            <Calendar CalendarDayButtonStyle="{StaticResource CalendarCalendarDayButtonStyle}" />
        </Viewbox>
    </Grid>
</UserControl>

CustomCalendar に依存関係プロパティ Events をサンプルとして定義します。

public partial class CustomCalendar : UserControl
{
    public static readonly DependencyProperty EventsProperty =
        DependencyProperty.Register
        (
            nameof(Events),
            typeof(ObservableCollection<Data>),
            typeof(CustomCalendar),
            new PropertyMetadata
            (
                new ObservableCollection<Data>()
            ));

    public ObservableCollection<Data> Events
    {
        get => (ObservableCollection<Data>)GetValue(EventsProperty);
        set => SetValue(EventsProperty, value);
    }

    public CustomCalendar()
    {
        InitializeComponent();
    }
}

スタイルで Events と連携させます。MultiBinding にするのは、1つ目が「マウスクリックで選択日付」で、2つ目が「Events」そのものを渡します。

<Style x:Key="CalendarCalendarDayButtonStyle" TargetType="{x:Type CalendarDayButton}">
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="120"/>
    <Setter Property="FontSize" Value="14"/>
    <Setter Property="FontWeight" Value="Bold" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type CalendarDayButton}">
                <Grid>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>

                        <ContentPresenter x:Name="NormalText" 
                                            Grid.Row="0"
                                            TextElement.Foreground="#FF333333" 
                                            Margin="1"
                                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
                        <TextBlock Grid.Row="1" Grid.RowSpan="2" 
                                    HorizontalAlignment="Center"
                                    FontWeight="Normal">
                            <TextBlock.Text>
                                <MultiBinding Converter="{StaticResource DateToCountMultiConverter}">
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="DataContext" />
                                    <Binding Path="Events" RelativeSource="{RelativeSource AncestorType=local:CustomCalendar}" />
                                </MultiBinding>
                            </TextBlock.Text>
                        </TextBlock>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

MultiConverter のサンプル。選択日付にあるイベントの「件数」を返却します。

public class DateToCountMultiConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length < 2 || values[0] == null || values[1] == null)
        {
            return "0";
        }

        if (values[0] is DateTime date && values[1] is IEnumerable<Data> events)
        {
            var count = events.Count(e => e.Date.Date == date.Date);
            return count.ToString();
        }

        return "0";
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

ElementNotAvailableException の対応

サンプルのコントロールのテンプレートを変更・削除しすぎて System.Windows.Automation.ElementNotAvailableException が発生しています。

この例外は、わかりやすい例だと DataGrid や ListBox なんかの行データの仮想化データを再表示するときに、(なにかが原因で仮想化した)データが存在しないときに発生する例外です。コントロールを自分でカスタマイズしていると、仮想化されるはずだったコードを削除してしまったり、仮想化するコントロールが無くなっていたりして起こることがあります。

この対処の一例は、仮想化しないようにします。

public class CalendarB : Calendar
{
    protected override AutomationPeer? OnCreateAutomationPeer()
    {
        // 空の AutomationPeer を返すことで Automation サポートを無効化
        return null;
    }
}
    <Grid>
        <Viewbox Stretch="Uniform">
            <local:CalendarB />
        </Viewbox>
    </Grid>

注意として、当然ながら仮想化によるパフォーマンス対策を消すことになる。

カレンダーの Binding 要素の更新

カレンダーの画面更新(日のセルに Binding したテキストを表示するなど)が必要になったときは、正直な話をすると直接的なよい更新方法を実装することができませんでした。

下記のようにすることで実装しておくことが(今は)一番適切だと思います。

public void ForceUpdate()
{
    var selectedMode = CalendarControl.DisplayMode;

    // なんでもいいから違うモードに一旦切り替えて、すぐ戻す
    CalendarControl.DisplayMode = CalendarMode.Year;
    CalendarControl.DisplayMode = selectedMode;
}

InvalidateVisual などでも更新してよさそうなのですが、メソッドまわりを適当に実行しても更新される様子はありませんでした。

Binding 自体を更新するなら BindingOperations.GetBindingExpression でどうかなとも思ったのですが、上記がてっとり早かった。

カレンダーの Binding の修正例

個人的な感想として、今回実装した Calendar のコントロールの Events の要素は、日ごとに要素の数を毎回数え直す必要があったため、あまりよい実装ではないと思います。

最終的には Dictionary で実装してみるほうが、まだよい気がします。

public static readonly DependencyProperty EventsProperty =
    DependencyProperty.Register
    (
        nameof(Events),
        typeof(Dictionary<string, Data>),
        typeof(CustomCalendar),
        new PropertyMetadata
        (
            new Dictionary<string, Data>()
        ));

Data の中で、その日のイベント要素をあらかじめコレクション化しておくほうがカレンダーコントールの Converter の処理としては単純になって、パフォーマンス面でデータ数が増えたときによくなる(守られる)のではないか、と思っています。

さわってみるとわかるけど、意外なところで Binding のイベントが発生しています。なので、パフォーマンス対策は気にしてもいいかもしれない。

サンプル

GitHub にサンプルコードを公開しています。

参考

sharplab-inspect の使い方

Sharplab は 2016 年後半ごろに「TryRoslyn」という名前で「C# を Web 上で試せるサイト」として一番よさそうなサイトとして有名になりだしたと思います。

今さらですが、Sharplab には、おまけで追加されている機能 Inspect があったりします。今回の記事では Inspect が、どういったことができるのか要点を整理しておこうという記事です。

余談ですが、Microsoft が公式に .NET Framework のソースコードを公開したのもこのくらいの時期だったように思います。(.NET は GitHub)この時期の C# は、今思うと大きな変化のあった時期になってますね。 - Microsoft Reference Source

1.実行コードの「Run」

コードの実行テストをするときは、まずこれ。このときに後述する Inspect を知っているとすこしだけ得をすることがあるかもしれません。

Result を「Run」に設定して、結果を確認します。コードが実行された結果が、思い通りかどうかをチェックします。

/*
  SharpLab tools in Run mode:
    • value.Inspect()
    • Inspect.Heap(object)
    • Inspect.Stack(value)
    • Inspect.MemoryGraph(value1, value2, …)
*/
using System;

Console.WriteLine("🌄");

output:

🌄

sharplab のコードの記入するところに「Run」モード中に使える sharplab の tools 機能 Inspect の紹介が書いてあります。しかし、使い方のドキュメントらしいものがありませんでした。とりあえず「Inspect」の機能は次の7個のうち3個です。

  • Allocations(Func action)
  • Allocations(Action action)
  • Equals -> NotSupportedException
  • Heap
  • MemoryGraph
  • ReferenceEquals -> NotSupportedException
  • Stack

Inspect

value.Inspect()

値のチェックをしたいときは、サンプルコードのように Console.WriteLine でもいいですが、変数の値を見たいだけなら value.Inspect() もけっこう便利です。

1.Inspect();
"aaa".Inspect();
"aaa".Inspect("title");

定義:

変数の値を Result に出力してくれます。引数はタイトルの名前を変える。デフォルトは "Inspect" になる。

引数:

  • Inspect()
  • Inspect(string)
    • string = タイトルの名前

Inspect.Heap(object)

指定したオブジェクトがヒープにどのように配置されているかを調べるためのメソッドです。特定のオブジェクトについて、メモリ上での配置や構造を視覚化できます。

using System;

var obj = new { Name = "abc", Value = 404 };
Inspect.Heap(obj);

定義:

対象のオブジェクトを受け取り、ヒープ内でのメモリ配置を調査します。

引数:

  • Heap(object)
    • object = 調査をする対象オブジェクト

Inspect.Stack(value)

指定した値がスタック上にどのように配置されているかを確認するためのメソッドです。構造体やローカル変数など、スタックに格納される値(値型)について、メモリ上の配置や内容を視覚化します。

using System;

var point = new Point { X = 10, Y = 20 };
Inspect.Stack(point);

struct Point
{
    public int X;
    public int Y;
}

定義:

任意の型 T の値を受け取り、その値がスタックにどのように配置されているかを調査します。

引数:

  • Stack(object)
    • object = 調査をする対象オブジェクト

Inspect.MemoryGraph(value1, value2, …)

指定されたオブジェクトのメモリレイアウトをグラフィカルに表示します。オブジェクトのフィールド、配列の要素、参照先などのメモリ上での関係を可視化することができます。

using System;

Node node1 = new Node { Value = 1 };
Node node2 = new Node { Value = 2, Next = node1 };

Inspect.MemoryGraph(node1, node2);

class Node
{
    public int Value;
    public Node Next;
}

定義:

複数のオブジェクトを受け取り、それらのメモリ上の相互関係をグラフとして視覚化します。

引数:

  • MemoryGraph(T value)
    • value = 調査をする対象オブジェクト
  • MemoryGraph(T1 value1, T2 value2)
    • value1 = 調査をする対象オブジェクト1
    • value2 = 調査をする対象オブジェクト2
  • MemoryGraph(T1 value1, T2 value2, T3 value3)
    • value1 = 調査をする対象オブジェクト1
    • value2 = 調査をする対象オブジェクト2
    • value3 = 調査をする対象オブジェクト3
  • MemoryGraph(T1 value1, T2 value2, T3 value3, T4 value4)
    • value1 = 調査をする対象オブジェクト1
    • value2 = 調査をする対象オブジェクト2
    • value3 = 調査をする対象オブジェクト3
    • value4 = 調査をする対象オブジェクト4
  • MemoryGraph(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5)
    • value1 = 調査をする対象オブジェクト1
    • value2 = 調査をする対象オブジェクト2
    • value3 = 調査をする対象オブジェクト3
    • value4 = 調査をする対象オブジェクト4
    • value5 = 調査をする対象オブジェクト5

値型と参照型の学習にとても役にたつツールだと思います。

Inspect.Allocations(補足)

Allocations は、現状ただしく動作していないように思います。

using System;

int Calculate()
{
    int[] numbers = new int[10];
    return numbers.Length;
}

void RunAction()
{
    string text = new string('a', 100);
    Console.WriteLine(text);
}

Inspect.Allocations(() => Calculate());
Inspect.Allocations(() => RunAction());
System.NullReferenceException: Object reference not set to an instance of an object.

定義:

TBD

オーバーロード:

TBD

参考

C# パフォーマンス高速化に関するメモ

この記事は「C# Advent Calendar 2024」に参加しています。シリーズ2の12月13日の内容です。

C# のパフォーマンス高速化に関することを学習した内容です。下記のような Tips があったので、個人的にサンプルコードを書いたり調べたりして学習した内容になります。(内容は加筆あり未記載もあり)

ポイントは「ソフトウェアは速くて困ることはない」

  • ヒープアロケーション(割り当て)を(避けれるところは)避ける
    • メモリの再利用とスタック領域の活用を徹底
  • 非同期 I/O を使う
    • CPU は高価なリソースなので、待ち時間を与えず使い切る
  • 計算効率を上げる

C# におけるメモリ管理方法は値型「スタック」と参照型「ヒープ」の2種類があります。一般的にヒープの領域の "確保" はスタックと比べると重たい処理になります。

別の言い方でのポイント:

  • 実装時からやることやって損しない
    • よく通るコードパス/共通ライブラリは特に目を向ける
  • 速いコードを書く癖をつける
    • 練習と思って取り組むほうがいい

読むときのための用語メモ:

値型の利点は、スタックに値を置く(ヒープを使わない)ことによる性能向上です。ボックス化(ヒープ領域の確保)が起きると、値型の利点は失われます。

パフォーマンスに影響の大きい絶対に守るべきこと

コレクションの型あわせ

List 型を配列型に変換する。不可避な理由がないなら無駄なメモリコピーが発生するため適切ではない。

List<int> values = [1, 2, 3, 4];

DoSomething(values.ToArray());

void DoSomething(int[] values)
{
    ...
}

ただし、後述の AsSpan()(Span<T>)は、逆に最適化になることがあります。

int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
foreach (var i in array.AsSpan()[2..6]))
{
    ...
}

ループ内の線形探索

コレクションのデータ数を考えて、たくさんデータがあるときはどうすれば取得が速いのか。First は線形探索なので効率とかじゃないよね。

foreach (var sample in samples)
{
    // BAD
    var bad = sample.First(p => p.id == id);

    // GOOD
    var good = sample[id];
}

線形探索……先頭から順番に比較を行い、見つかれば終了する。

コレクションの初期容量を指定

動的なバッファ拡張は、なるべく避ける。

初期容量 (capacity) を指定しなければ、最後の要素を追加するときに2倍の内部バッファ確保と全要素のコピーが発生するため非効率。

パフォーマンスに悪影響があるのは、 Count 3 のタイミング。コレクションの容量を上げるために、既存コレクションから新しいコレクションにコピーが発生する。

int[] values = [1, 2, 3, 4];
var list = new List<int>(capacity: values.Length / 2);

foreach (var x in values)
{
    list.Add(x);
    Console.WriteLine($"Capacity: {list.Capacity}, Count: {list.Count}");
}
Capacity: 2, Count: 1
Capacity: 2, Count: 2
Capacity: 4, Count: 3
Capacity: 4, Count: 4

Result と Wait はやめる

スレッドの処理をブロックするので、スレッドが占有される。想像しているよりもデッドロックしやすい。

結果的にリソース効率が低下する。

public int DoBadSomething()
{
    var result = CallAsync().Result;
    return result + 1;
}

public async Task<int>  DoGoodSomething()
{
    var result = await CallAsync();
    return result + 1;
}

本質的ではないけど、デッドロックを避ける対策の一例。処理するスレッドが変えて待つけど、スレッドが効率的に利用されているとは言えない。

public int DoBadSomethingSafe()
{
    var result = CallAsync().ConfigureAwait(false).GetAwaiter().GetResult();
    return result + 1;
}

環境

最速の開発/実行環境

最新の .NET を利用しておくこと。

.NET Framework よりも何倍も速い。C# 7.x 以降は、パフォーマンスを意識した改善が多い。基本的には後方互換があるので更新して最新の言語機能を使う。

  • MemoryPack
  • MessagePack for C#
  • FastEnum

構造体

Understanding C# Struct All Things より

構造体の優位性

ガベージコレクションの頻度を下げる

  • GC の実行時はパフォーマンスが瞬間的に大きく低下する
  • 特に Unity のようなフレームベースのアプリは気をつける

メンバーへのアクセスが高速になる

  • 値型:スタック領域にあるので、そのまま直にアクセス可能
  • 参照型:ヒープ領域から値をたどってアクセスする

思ったよりも構造体の挙動は、言語仕様の知識が必要なところがあります。いくつかの作法を知っていることで、効率化される感じです。

構造体の欠点

高頻度で値のコピーが発生する

  • 引数/戻り値/変数への代入/関数呼び出し
  • サイズの大きい構造体の場合は、逆にコストになる(16 bytes 目途)

継承/多態ができない

  • 現状は interface + 拡張メソッドを駆使する

インターフェース以外の継承ができない理由を考えると、struct は「メモリをマッピングした構造だから」という考え方がわかりやすいと思います。裏付けとして StructLayout はデフォルトだと Sequential です。(class のように)データの並びをアライメントにあわせた最適化はされません。値型として、固定サイズを守ることが優先されると考えることができます。

インターフェースを付与できるのは、メモリ構造に影響を与えずに多様性を追加するだけだからです。もともと、C言語の構造体は完全な値型でメソッドを作ることができませんでした。しかし、C#の構造体は最初から、値型としての特性を持ちつつメソッドを持てます。同じ理由だと考えています。

参照渡し

性能劣化に直結するコピーを抑制する。構造体のサイズが大きいときは検討する。

具体的にすると以下の例だと a は PassThrough メソッドに参照を渡していて a のメモリ上のアドレスが共有される動きになります。

さらに ref var b は a を参照しているから a と b は同じ値であり、b を変更すると a の値も書き換わる。

private void TestRef()
{
    var a = 1; 
    ref var b = ref PassThrough(ref a);

    // b を変更すると a にも影響する
    b = 2;

    Console.WriteLine(a); // output 2
}

private ref int PassThrough(ref int b)
{
    ref var c = ref b;
    return ref c;
}

ref 戻り値は C# 7.2 から利用可能になったので 2017 年末からの機能です。また ref readonly のような戻り値の書き方もできる。(が、後述のようにパフォーマンスの観点では注意が必要)

このほかにも「参照渡しだけど読み取り専用」なら in を使えるので、大きい構造体を渡すときはパフォーマンス改善につながる引数の指定になる。

従来の out は「参照渡し」。あるメソッドの中で、メソッド外の変数の値を書き換えることができるのに対して in は「入力用」を明示します。

コンパイラの動作としては in/out は ref なので、引数違いのオーバーロードはできない。

補足として、構造体ではなくクラスの場合はあまりパフォーマンスの改善に効果がない。理由は参照型なのでポインタのような動きをするヒープ領域を利用しているため、データそのものがコピーされることがない。

ref readonly/in の難しさ

Defensive Copy が発生してしまう。

引数の場合は in で、戻り値の場合は ref readonly として、この問題がどちらの場合でも発生してしまう。(後述の Generics は別)

プログラムの都合として、呼び出し側は、メソッドの内部で値が書き換わっていないことを保証する方法を持たないので、あらかじめメソッドを呼んだ時点で無条件にコピーを作ってしまう動きのことを Defensive Copy という。

対策:

  • 構造体を readonly にしておく
  • 関数内でフィールドの書き換えがないことを保証しておく
readonly struct ReadOnly
{
    public readonly int X;
}

struct Foo
{
    ..
    public readonly int Add() => X + Y; // GOOD
    public int Sub() => X - Y; // BAD コピー発生
    ..
}

コピー抑制

構造体の拡張メソッドを作るといは、参照渡しにする。Generics における in 引数はできない。

これはコンパイラが型は値型か参照型かを判断できていないので、仕様として in 引数にできない。(参照型のときは参照渡しである必要がない)

static void Process<T>(in this T data) // エラー
{
}

static void Process<T>(ref this T data) : where T : struct // OK
{
}

in を使った overload をするときも、struct 自体を readonly にしておく。

readonly struct Complex
{
    public double R { get; }
    public double I { get; }

    // in 引数が認められるようになった
    public static Complex operator +(in Complex x, in Complex y)
        => new Complex(x.R + y.R, x.I + y.I);
}

というより、基本的には in は readonly な struct に使うもの、という考え方になるはず。

ValueTuple

匿名型の歴史みたいなもの。LINQ といった局所的なところで使うとよいもの。ValueTuple は、値を変更できる構造体として考えておく。

コレクションからコレクションを選択する動作なうえ、ValueTuple はヒープを使わないから、すごくパフォーマンス上の相性がいい設計だと思う。

// ValueTuple : スタック利用
var q1 = collection.Select(x => (value: x, power: x * x));

// 匿名型: ヒープ利用
var q2 = collection.Select(x => new { Value = x, Power = x * x });

// Tuple : ヒープ利用(名前を付けられない)
var q3 = collection.Select(x => Tuple.Create(x, x * x));

Span

連続したメモリ領域を直接参照できる。unsafe じゃない managed な状態でアクセスできる。配列や文字列から、メモリの再確保をしない部分的な参照ができる。

const string text = “ab123cd”;
var span = text.AsSpan(2, 3);
var sub = text.Substring(2, 3); // 123

中身を read するだけなら span を上手く活用することで、無駄なコピーを避けて高速化させることができる。

でも、内部的にコピーが発生しているかどうかなんて、言語の理解が深まらないと最適化できない。

string.Create

string.Create は Span を使っているので、無駄なオブジェクトを生成させることなく文字列を作成することができる。

以下は byte 型の値を文字列に変換する(例えば、文字列 "01101100" にする)もので buffer が span なので StringBuilder のような中間変数が存在しない。とてもテクニカルな技術だと思った。

static string ToBitString(byte value)
    => string.Create(8, value, (buffer, state) =>
{
    const byte on = 0b_0000_0001;

    for (var i = 0; i < buffer.Length; i++)
    {
        buffer[buffer.Length - 1 - i]
        = ((state >> i & on) == on) ? '1' : '0';
    }
});

ボックス化を回避する

古い産廃を使ってはいけない

  • System.Collections
    • ダメ絶対
  • System.Collections.Generics
    • 変更先

非 generics コレクションなので使わないほうがいい。名前空間が出ていたら注意したほうがいい。

System.Enum の問題点

値を代入するとボックス化してしまう。

// ボックス化する
static void Foo(Enum value)
{}

// Generics 制約をするとボックス化しない
static void Foo<T>(T value) where T : struct, Enum
{}

構造体を interface 型として扱う

正直、構造体の高速化テクニックはマニアックだと思う。

struct は object するポイントとしてインターフェースを経由すると Box 化が発生してしまう。が、そこで generics を使うと脱仮想化になる。

// 引数でbox 化が発生して遅い
static void Interface(IDisposable x)
    => x.Dispose();

// .NET Core 2.1 以降の場合
// 脱仮想化という最適化がかかる
static void NonGeneric(X x)
    => ((IDisposable)x).Dispose();

// 安定して高速
static void Generic<T>(T x)
    where T : IDisposable
    => x.Dispose();

一番よく言われるようなのが、比較の equal 関係なので、そういったところは record を使うことでも回避できると思います。

知らない間に作られるインスタンスに気を配る

クロージャの変数生成

変数のキャプチャー = 隠しクラスの生成ということになります。なので、ヒープ確保と利便性はトレードオフの関係です。

具体的には、ラムダ式や匿名メソッド内で外部スコープの変数を参照するとき、変数がクロージャにキャプチャされることを指しています。この「クロージャ」はヒープ上にオブジェクトとして生成されることになります。

以下は id がキャプチャされる例です。Visual Studio では "=>" の部分にカーソルを合わせると「キャプチャされているかどうか」を知ることができます。

static async Task<Person> GetAsync(int id)
{
    var people = await QueryFromDbAsync();
    return people.FirstOrDefault(x => x.Id == id);
    }

なので、フィルタリング部分条件をラムダ式の外に置く、拡張メソッドにして自作をしたりすることになる。

static async Task<Person> GetAsync(int id)
{
    var people = await QueryFromDbAsync();
    foreach (var person in people)
    {
        if (person.Id == id) return person;
    }
    return null;
}
public static T? FirstOrDefault<T, TState>(
this IEnumerable<T> source, TState state, Func<T, TState, bool> predicate)
{
    foreach (var x in source)
    {
        if (predicate(x, state))
        return x;
    }
    return default;
}

また、ラムダ式や匿名メソッドは外の変数をキャプチャできてしまうため、変数名として x や y や i を使ったつもりが、外の変数をキャプチャしてしまってバグになることがあります。そういうときは、静的ラムダ式を使うことで、外部スコープの変数が入り込まないように(ある種)明示することになります。

var x = 10;
var result
    = Enumerable.Range(0, 10)
    .Where(static x => x % 2 is 0)
    .ToDictionary(static x => x, static y => x * x); // 警告になる

時間とリソースを有効に使う

非同期 IO を使う

await している間にできることがある。

// これなら通信待ちを別処理に有効活用できる
var url = "....";
var client = new HttpClient();
var rss = await client.GetStringAsync(url);
var node = XElement.Parse(rss);

並列処理

それぞれの処理が独立しているなら、Parallel にすると処理時間を短縮できるかも。

var t1 = LoadFile1Async();
var t2 = LoadFile2Async();
var t3 = LoadFile3Async();

await Task.WhenAll(t1, t2, t3);

キャッシング

まず考え方として Enum.GetValues はリフレクションを内部的に利用しているため、比較的コストが大きい操作になっています。

なので、この処理を繰り返し実行することは避けたい。そこで、キャッシングを利用することで、一度だけ値を取得して、以降は保存された値を使うことでパフォーマンスを上げる。

public static class FastEnum
{
    public static IReadOnlyList<T> GetValues<T>()
        where T : struct, Enum
        => Cache<T>.Values; // キャッシュを直接参照

    // 静的 Generics 型のフィールドにキャッシュを持つ
    private static class Cache<T>
    where T : struct, Enum
    {
        public static readonly T[] Values;
        static Cache()
            => Values = (T[])Enum.GetValues(typeof(T));
    }
}

ただし、キャッシュするサイズが大きくなるとメモリ消費が大きくなる。キャッシュされた値が変更される場合があると、キャッシュの無効化や更新が必要になるのでよくない。

List の高速イテレーション

コレクションのデータを Add/Remove/Clear などの操作をせずに、大量のデータにアクセスする場面では Span が有効なことがある。リストを変更しないことは重要。

List<int> list = [0, 1, 2, 3, 4];
var span = CollectionsMarshal.AsSpan(list);

foreach (var x in span)
{
// Do something with x
}

Frozen Collections

Frozen Collection はコレクションを作成した後は変更ができません。スレッドセーフでマルチスレッド環境で安全に利用できるのも特徴。

データが固定化されたことで、検索が最適化されています。Contains なども高速化。

単純なことなので、Frozen できることを覚えていれば OK だと思います。

var data = new Dictionary<int, string>
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" },
};

var frozenData = data.ToFrozenDictionary();

構造体は yield return しない

これもおそらく構造体を利用することでヒープアロケーションを回避するテクニックだと思います。値型なので、キャッシュ効率が挙がって、頻繁なイテレーション部分で性能を向上させるテクニックのはずです。

foreach や LINQ は GetEnumerator を使ってイテレーションを開始するので、構造体が返されるときは、ここまでの例のようにインターフェース型 IEnumerator<T> として扱われると、暗黙的にボクシングが発生する効率の悪さもある。

yield return は状態マシンなので言わんとするところは同じはず。

class Program
{
    static void Main()
    {
        var range = new RangeEnumerable(1, 10);

        foreach (var num in range)
        {
            Console.WriteLine(num);
        }
    }
}
public struct RangeEnumerable : IEnumerable<int>
{
    private readonly int _start;
    private readonly int _end;

    public RangeEnumerable(int start, int end)
    {
        _start = start;
        _end = end;
    }

    public Enumerator GetEnumerator() => new Enumerator(_start, _end);

    // 明示的に IEnumerator<int> を使いたい場合
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    // 内部構造体で IEnumerator<int> を実装
    public struct Enumerator : IEnumerator<int>
    {
        private readonly int _end;
        private int _current;

        public Enumerator(int start, int end)
        {
            _current = start - 1;
            _end = end;
        }

        public int Current => _current;

        object IEnumerator.Current => Current;

        public bool MoveNext()
        {
            if (_current < _end)
            {
                _current++;
                return true;
            }
            return false;
        }

        public void Reset() => throw new NotSupportedException();

        public void Dispose(){}
    }
}

補足:C# のコンパイラは、特定のインターフェース型を直接指定しなくても、対応するメソッドやプロパティが存在していれば、コードを正しく動作させます。これは型が厳密にインターフェースを実装している必要が無いという点で "duck typing" と呼ばれるものです。

参考

WinGet を利用した Python の更新(windows 編)

Windows 環境で Python を更新するときの流れを記録しました。

更新の必要有無を確認

とりあえず、デフォルトの Windows 環境で Python のチェック方法を記録します。コマンドプロンプトを開いて、バージョンをチェックする。

python -V

ここで「python3- V」というコマンドが紹介されていることもあります。Windows 環境では Python 3.X をインストールすると python コマンドが割り当てられるみたいです。python3 は自動的に割り当てられないことがあります。

自分の環境では以下のようになっていました。Windows Store App の Python だけだと、コマンドプロンプトで「python3 -V」を入れたときに「Python」が返ってくる状態になるみたいです。

where python3
> C:\Users\#user-name#\AppData\Local\Microsoft\WindowsApps\python3.exe
where python
> C:\Users\#user-name#\AppData\Local\Programs\Python\Python312\python.exe
> C:\Users\#user-name#\AppData\Local\Microsoft\WindowsApps\python.exe
py -0p
-V:3.12 *        C:\Users\#user-name#\AppData\Local\Programs\Python\Python312\python.exe
コマンド python バージョン 説明
python 環境依存 (3.X or 2.X) デフォルトの python を実行
python3 3.X Python 3.X を明示的に実行(主に Linux 用)
py 最新の python バージョン管理が可能 (主に Windows 用)

なんで、Windows 環境では py コマンドが python3 コマンドみたいなものなので、便利だと思います。とはいえ、バージョン更新までしてくれるわけではない。

一般的には、それは「役割が違うよね」ということになるので、やはり「パッケージマネージャ」に頼る必要があります。しかし、Windows はデフォルトだとパッケージマネージャの用意がない(はずな)ので、結局マニュアルで HP からダウンロードしてアップデートすることになる……のですが、今回は「winget」を利用してアップデートする例を示します。

winget

すこし前までは windows 環境でパッケージマネージャーをしようとするとサードパーティの Chocolatey や Scoop を利用することになったと思います。

winget は microsoft が提供しているパッケージマネージャーで、徐々に使われるようになってきていると思います。

winget のバージョン確認は、以下のどちらかのコマンドをコマンドプロンプトで実行してください。もしも、インストールされていなかったら、Windows Store からインストールします。Windows Store アプリであれば、winget 本体のバージョン管理は自動的に更新されるようになるはずなので「楽」です。

winget
winget -info

コマンド:

コマンド 説明
install 指定されたパッケージをインストール
show パッケージに関する情報を表示します
source パッケージのソースの管理
search アプリの基本情報を見つけて表示
list インストール済みパッケージを表示する
upgrade 利用可能なアップグレードの表示と実行
uninstall 指定されたパッケージをアンインストール
hash インストーラー ファイルをハッシュするヘルパー
validate マニフェスト ファイルを検証
settings 設定を開くか、管理者設定を設定する
features 試験的な機能の状態を表示
export インストールされているパッケージのリストをエクスポート
import ファイル中のすべてのパッケージをインストール
pin パッケージ ピンの管理
configure システムを適切な状態に構成します
download 指定されたパッケージからインストーラをダウンロードする
repair 選択したパッケージを修復します

オプション:

オプション 説明
-v,--version ツールのバージョンを表示
--info ツールの一般情報を表示
-?,--help 選択したコマンドに関するヘルプを表示
--wait 終了する前に任意のキーを押すプロンプトをユーザーに表示
--logs,--open-logs 既定のログの場所を開く
--verbose,--verbose-logs WinGet の詳細ログを有効にする
--nowarn,--ignore-warnings 警告出力を非表示にする
--disable-interactivity 対話型プロンプトを無効にします
--proxy この実行に使用するプロキシを設定
--no-proxy この実行に対するプロキシの使用を無効にする

æ›´æ–°

winget upgrade --all
winget upgrade --all --silent

初回はサイレントにしなくてもいいかもと思います。サイレント実行をすると、管理者権限が必要になる場合もあってこのときは Powershell の管理者モードで実行するといい。

サイレントにしていない場合は、各ソフトウェアのポップアップが表示されるので、邪魔に思うかもしれないけど初回はこれくらいのほうが安全じゃないかな、という意見です。

これで python もまとめてアップデートできる。正直、アップデートをしない半端なバージョンを許さないほうがいいと思う考えで、バージョンを固定したいときは、下記の pin であらかじめ固定しておいて、他は --all ですべて更新がよいと思っています。

どうしても個別にやりたいときは python を例に挙げると、最初に list で ID を確認します。

winget list python

確認できた ID に対して upgrade を実行する。

winget upgrade --id Python.Python.3

念のため、最後にコマンドプロンプトでバージョンが変わっているかどうかもチェックしておこう。

python -V

バージョンの固定

アップデートしないパッケージは pin を使ってバージョンを固定します。

winget pin add <package>

こうしておくと upgrage --all といったアップデートの対象外になる。pin に追加した一覧を確認したいときは以下。

winget pin list
コマンド 説明
add 新しい PIN を追加する
remove パッケージ PIN の削除
list 現在のピンを一覧表示する
reset PIN をリセット

おまけ:インストールのバッチ化

winget で管理できるアプリは「winget.run」で探す。

簡単な .bat ファイルを作成することで、ソフトウェアのインストール状態をまとめることが可能なので便利だと思います。

echo off
winget install -e --id RARLab.WinRAR
winget install -e --id Google.Chrome
winget install -e --id Google.Drive
winget install -e --id OBSProject.OBSStudio
winget install -e --id Dropbox.Dropbox
winget install -e --id Greenshot.Greenshot
winget install -e --id Notion.Notion
winget install -e --id KeePassXCTeam.KeePassXC
echo on

参考

2024 年 会社勤めの買ってよかったモノ、まとめ

12月の風物詩。毎年やっているけど、買って損していないのかチェックみたいな気もする。

QOL

文具

ぬいぐるみは、ドラキー、ももんじゃ、スライムの3匹を購入して本棚を守ってもらっています。おばけきのこを追加したい。

水出しポットは、会社で飲むコーヒーを作り置きするスタイルに。薄い味のコーヒーが好きなので、人の倍は豆が長持ちしてしまう。

マグカップもコーヒー関係で同じ。今年はコーヒーの参考書も読んで、すこし勉強もしていました。

レバーレスコントローラー作成

友人にレバーレスコントローラーを作成してみたりもして、ゲームのモチベーション自体はあるつもり。

PC 関係、スマートフォン

スピーカーは主に風呂場で髪を切ったり、本を読んだりするときに利用しています。冬の長風呂はヒートショックのリスクがあるみたいですが、すきなんです。

雷ガードは合計8つは購入したのかな。とりあえず、コンセント大本に装着してまわって雷対策を実施。電源タップが必要なところは、埃ガードのものに更新。私室の電源タップは、5~6年くらい経過していたのでこっちも更新しました。

ノイキャンイヤホンは、前に使っていたものが触ったときのイベントが検知できなくなったので更新。前のやつは家専用に。

運動

ハンドグリップはついに「1」になった。毎年くらいのペースでひとつずつ重さを上げています。暇なときにギコギコしてる手遊び。

ホエイプロテインは、今年けっこう買ってた。ブラックフライデーのセールでも1万円分購入。

本

分野がぐちゃぐちゃ。今はメンタルヘルスの資格試験の参考書と MBTI に興味がわいたので、こっちも読んでいます。

マンションに関する情報はプライベートでいろいろあって、すこしずつ情報収集をはじめました。日本の不動産の仕組みは同意できないところが多くて、イヤな気持ちにさせられる。

漫画は、あと「ポンコツ魔王の田舎暮らし」が気になっているので購入しようかなぁって気持ちになっています。おもしろいのは報われてほしいし、ちゃんと続いてほしい。

総括

2024年は、久しぶりに資格試験を受験して「第一種衛生管理者」を取得しました。このあともメンタルヘルス関係の民間資格の受験を検討しています。そのほか、専門分野外の参考書を読む機会もあり、学習を例年よりもがんばることができたと思います。

プログラミング関係も、興味とやることに悩むところがあったけど、すこしずつ学習して成長を目指すことができた。

「プライベート」では、有馬温泉の旅行は結構よかったし、ちょこちょこと遊びにでかけた。格闘ゲームの流行に乗りたい気持ちがあったものの、学習のタイミングもあって時間を上手くつくれなかった一方で、後半はモンスターハンターの新作に備えて時々1~2Hの時間を作って友達と IB で操作練習を開始していたり、勉強・仕事・遊びのバランスが未熟ながらいくらか取れていたと思う。

「投資」に関しては、NISA の制度が新しくなったけど、速やかに1月から即対応できたのはよかったと思う。個別銘柄を長期で保有する予定のものは現状追加していないつもりだけど、任天堂やDNAといった銘柄をスポット的なタイミングで短期の売買したりまだ保有をしており、年間で数十回の売買をした。配当金以外のところでも配当金以上の黒字をだせたのは、経済的な動きを知るうえでもよかったと思う。ここは、去年書いたことを守ることができた。

ビジネス面は、忙しくなる予定だったものが半年以上ずれ込んだこともあって、忙しさの最中にまだいます。年始からも忙しい状態が継続するはずなので、体調やメンタルに気を付けて健康的にやっていきたい。(このあたりは、予感があったので衛生管理者やメンタルヘルスの学習で対策をすすめています)

来年は、「学習」だとメンタルヘルスの資格試験を受験したい。「投資」に関しては継続と参考書を1冊はしっかり読む。「仕事」は嫌なことも増えてるけど考えすぎずに、やれることをやっていく。あと、「脱毛」はとりあえず自宅用のやつからでもいいので取り組んでいく。

「遊び」は MONSTER HUNTER Wilds にまず注力やな。

履歴

第一種衛生管理者試験の受験(合格)

2024 年 10 月15 日 (火) に第一種衛生管理者試験を受けてきました。(試験範囲が狭い)第二種衛生管理者は受験していないので、上位資格といっていいだろう第一種だけ受験した形です。

結果は「合格」

よかったです。試験結果の得点、各問題の正解/不正解の記載は、ありませんでした。(別途費用で結果を請求できるみたいですが……)

受験に際しての勉強は、以下の通りだったとメモ。参考書を1冊読むのに(通勤時間などを利用して)ダラダラと1か月。試験の3週間前から、過去問と YouTube を使った勉強を(ようやく)はじめた感じでした。

豆知識として、コロナ禍の影響で受験者が増えていた試験みたいです。なので、人が増えたことで問題は少し以前より難化傾向にあったらしい?

第一種衛生管理者とは

労働安全衛生法において定められている、労働環境の衛生的改善と疾病の予防処置等を担当し、事業場の衛生全般の管理をする者、またはその国家資格。

50人以上の中小企業から1人以上の設置が必要になる資格なので、国家資格のわりに合格率は高く第一種衛生管理者はだいたい45%前後くらい。

一応、第一種衛生管理者は「受験資格」の確認が必要です。一番のおススメはコード番号「8」で、会社で10年以上労働衛生の実務に従事したことがあれば、会社の「事業証明書」だけで受験可能です。大学の卒業証明書とかも必要なくなるから雑費も下がるし簡単。受験申請は WEB システムからできるようになったので、受験まですることは「事業証明書」等をまとめて特定記録郵便で送ることだけだったと思います。(試験料+特定記録郵便費用)

私は、出張試験を WEB システムから予約して利用しました。受付期間(開始日)をリマインドしておいて、席が埋まる前に、ササっと予約するのがおススメです。

特定記録郵便は、郵便局でしかできないはずなので、ポストではなく郵便局に直接行かないといけないのはすこし手間だった。

受験した理由

私は、普段のブログの内容のとおりエンジニア(プログラマ)として仕事をしているので、衛生管理者としてどれだけ直接活動するかというと微妙なところです。

ただ、会社に所属する人なので。所属する会社の社内規則とは、どうしても付き合わないといけない。(もちろん労働基準法も)第一種衛生管理者は、会社の安全/衛生委員会などでも使えるし、会社の居心地のよさを自分で上げるために使える小ネタだと思いました。(内申点も調整できる)

また、基本的な労働基準法を学習できるし、その周辺知識や労働安全についても全般的にひろく学べる。労働者としての防御力があがる資格だと考えました。

勉強に使ったもの

4つ。申請から約2か月弱の短期間だったので、これだけでした。

試験の参考書は、なんでもいいと思っています。第一種衛生管理者は、色彩検定とかと同じで、毎回の試験内容は抜本的に変更されるようなものではありません。毎回そう違わない。なので、私が読んだ参考書は、近所の古本屋に置いてあったやつで、2022年用の参考書(100円)でした。

たしかに試験の内容は法律やガイドラインの変更によって、問題変更なされるのは事実だと思います。ただし、法律やガイドライン等の変更があった直後に新傾向の問題が出題される傾向は少ない or 無いみたいです。不文律としておよそ1~2年後に新傾向の問題として出題される雰囲気みたい。

補足として、私の(今回の試験の)参考書は、しっかりと隅々まで読み込むことはしていない。試験の過去問を解いたり、動画を視聴するときに、どの辺のページにあった内容だな、というのがわかればよし。あとで、過去問(公表問題)を解いたときに間違った問題や気になった問題は、参考書を開いて、ササっと再学習しやすいようにしておく感じ。

第一種衛生管理者は、過去問を解かないと話にならない、とよく言われています。なのでどのような方法でもいいから過去問は解いたほうがいいと思う。実際、安全衛生技術試験協会が公開している公表問題と本番の試験問題は、かなり似ていたと思います。(公表に無い問題も当然ながら出題されます)

自分はネット上に公開されていた公表問題(過去問)を過去3回分をしっかり解いた。参考書をザっと読んだだけだと、初回はかなり間違ったので50%以下だったと思う。間違った問題に限らず、すべての問題の選択肢をあらった。なぜ正解でなぜ不正解なのか選択肢をひとつひとつ完璧にした。結構時間はかかったけど、公表問題でわからないところは無いくらいはできたと思う。(暗記不足はあったかもしれないけど)

「第1種衛生管理者試験問題」は、この公表問題の学習をサポートしてくれている。過去の類似問題もチェックしやすいので重宝した。

YouTube は「 YouTube - 第1種衛生管理者合格チャンネル」に絞って視聴し、他はどこも見ていない。塾講師っぽい話し方で嫌味がなく丁寧でよかったと思います。ゆっくり話してくれるので1.5倍速くらいでも大丈夫。問題の文章もしっかりと言葉にして読んでくれるので、主に通勤の電車の時間やランニングやウォーキングをしながら聞いていました。思ったよりも効果的なのでおススメ。(有料プランは未加入でした。すいません)

学習期間

  • 参考書の読書:2024/08/19 ~ 2024/09/17
  • 勉強:2024/09/28 ~2024/10/14
  • 勉強の内訳:
    • 参考書(10%)
    • 公表問題(65%)
    • YouTube 動画(25%)

期間中、9月の上旬と下旬は、有馬温泉などの複数の旅行があって勉強していない。たのしかった。

第一種衛生管理者の試験は「関係法令」「関係法令(有害)」「労働衛生」「労働衛生(有害)」「労働生理」の5つの科目(区分)があり、それぞれで40%以上の点数であること、かつ、合計点が60%以上であるなら合格。仮に合計得点が合格点以上でも、どこかの科目が死んでると不合格になるのは油断ならない点だと思います。

問題は、10問 or 7問(有害)で合計44問だったはずです。なので、7問しかない(有害)は40%足切りに注意が必要だと思います。苦手問題や引っ掛け問題で死にやすいと思う。

また、このほか自分に関係のありそうなガイドラインは直接資料を開いて確認することもあった。たとえば「情報機器作業における労働衛生管理のためのガイドライン」など。この種のガイドラインの内容は、職場の同僚と確認したりした。

費用

  • 受験料 8,800 円(※会社の資格取得補助)
  • 参考書 100 円
  • 郵便(受験票取得:特定記録郵便244円+免許書取得:特定記録郵便490円=734円)
  • 免許書発行手続き、(返送用切手、収入印紙、写真)(約460+1500+1000=3960円)
  • 交通費 - 円

合計:約13,594 円

個人的には、受験料と免許発行手続き等は、会社の資格取得補助金を利用したので、自己負担は実質 1,000~2,000 円くらいだと思う。とはいえ、完全に自力で取得する場合でも 1.5 万円で高くはない。

受験の検討時点の話だと、受験票の取り寄せ、免許書発行手続きが「面倒そうだな」という予想は、正しい予想でした。実際けっこうめんどくさい。心を無にしてやる雑務の感じがある。どうしてもわからなかったら、安全衛生技術試験協会か郵便局に電話して聞こう。

参考