trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

EFFECTIVE C# 6.0/7.0 読書メモ 項目18

項目18 最低限必須となる制約を常に定義すること

ジェネリック型を作成する場合、最低限必須となる制約を常に定義する。

制約

メリット
使用者やコンパイラに対して想定する機能を伝達することができる。

制約があればコンパイラはその制約が満たされている*1として生成するため、使用者がジェネリック型を誤用する場合、コンパイルエラーとして検出できる。

デメリット
使用者は制約を満たす必要があるため、制約が多いと使い勝手の悪いものとなる。


ユーザのコスト(制約を満たすためにかかるコスト)と安全性や製作者のコスト(型を確認するためにかかるコスト)を考え、必要最低限となるような制約を指定すること。

制約がある場合とない場合の例

IComparable<T>のメソッドを呼び出して比較するメソッド。

制約がない場合

public static bool AreEqual<T>(T left, T right)
{
    //省略
    if (left is IComparable<T>)
    {
        if (right is IComparable<T>)
            return left.CompareTo(right) == 0;
        else
            //省略(実行時エラー)
    }
    else
        //省略(実行時エラー)
}

Tが期待した機能を持っている型*2か確認が必要となる。持っていない場合、実行時エラーとなる。

制約がある場合

public static bool AreEqual<T>(T left, T right)
    where T : IComparable<T> =>
    left.CompareTo(right) == 0;

メソッド中でTの確認が不要*3となるため単純化した実装にできる。

制約の個数を最小にする方法

  • 制約なしでも利用可能な機能であればそれを制約とはしない

例1)Equals()
IEquatable<T>.Equals()System.Object.Equals()が存在する。 IEquatable<T>.Equals()を使用するほうがメリットが大きいが、System.Object.Equals()でも十分に機能する(場合がある)。この場合IEquatable<T>制約を指定するのではなく、IEquatable<T>.Equals()が利用できるならそれを使用し、利用できないならSystem.Object.Equals()を使用するようにすればよい(追加の作業(型引数が機能を持つか確認)が必要になる)。

例2)new()制約
new T()を使用する代わりにdefault(T)を使用できるかもしれない。

*1:制約の機能を使用できる。制約がない場合、System.Objectに定義されたメソッドしか備えてないとして処理する

*2:IComparableメソッドを継承している

*3:Tが期待した機能を持っている型でない場合、コンパイル時エラー

EFFECTIVE C# 6.0/7.0 読書メモ 項目17

項目17 標準的なDisposeパターンを実装する

アンマネージリソースを持つ型のメモリ管理について。標準的な方法(Disposeパターン)を実装すること。

Disposeパターンの利点

型のユーザはIDisposableインターフェイスによりアンマネージリソースを適切なタイミングで解放できる*1。解放し忘れた場合も、ファイナライザ(デストラクタ)により非マネージドリソースを解放できる*2。また、IDisposableインターフェイスにより解放した場合、パフォーマンスの低下を最小限に抑えることができる*3

Disposeパターン詳細

docs.microsoft.com

ファイナライザの注意点
  • 非マネージリソースを扱う場合に限りファイナライザを実装すること。ファイナライザがあるだけでコストになる(GCの処理が増える)
  • ファイナライザではリソースの解放のみ行うこと。それ以外の処理を行うと、オブジェクトの参照を保持し、生存期間を延長させてしまうことがある。そうなると将来的にバグを生む可能性が高い。
ファイナライザを持つ型に対するGCの処理*4
  1. ファイナライザを持つオブジェクトをファイナライザキューへ追加
  2. ファイナライザキューに追加したオブジェクトのファイナライザを呼び出す新しいスレッドを開始
    (この時点ではメモリから削除されておらず、GCを生き残ったので世代が上がる)
  3. ファイナライザを実行し終えていたら、ファイナライザが不要なオブジェクトとしてマーク
  4. 上位の世代に対するガベージコレクションが実行された時点でオブジェクトが削除される
その他

親クラスでファイナライザをオーバーライドしていれば派生クラスでのオーバーライドは不要か(親クラスのファイナライザが実行され、派生クラスのDispose(bool)が呼ばれるため)

*1:マネージリソースに対しても同様

*2:ファイナライザはアンマネージリソースが確実に解放されるようにするための唯一の方法

*3:Dispose()メソッド内でGC.SuppressFinalize(this)を呼び出す。これは指定したオブジェクトに対してGCがファイナライザを呼び出さないように要求する処理

*4:Dispose()内でGC.SuppressFinalize(this)が呼ばれてない場合

EFFECTIVE C# 6.0/7.0 読書メモ 項目16

項目16 コンストラクタ内では仮想メソッドを呼ばないこと

コンストラクタで仮想メソッドを呼び出したときの動作について。

class B
{
    protected B()
    {VFunc();}
    protected virtual void VFunc()
    {Console.WriteLine("B内のVFunc");}
}
class Derived :B
{
    private readonly string msg = "初期化子で設定";
    public Derived(string msg)
    {this.msg = msg;}
    protected override void VFunc()
    {Console.WriteLine(msg);}
}

上記のような、コンストラクタで仮想メソッドを呼び出すクラスBから派生したクラスDerivedを作成すると、

> var d = new Derived("メイン内のコンストラクタ");
初期化子で設定

となる。

Dereivedを作成したとき、

  1. msgを初期化子で設定(msg = "初期化子で設定")
  2. 親クラスBのコンストラクタ実行(DerivedのVFunc())
  3. Derivedのコンストラクタ実行(this.msg = msg)

の順に実行される。

説明
親クラスのコンストラクタでは実行時におけるオブジェクトでオーバーライドされた仮想メソッドを呼び出す。上記の例では、 作成対象となっているオブジェクトはDerived。つまり実行時におけるオブジェクトの型はDerivedなのでDerivedのオーバーライドメソッド(VFunc)が呼ばれる。

このとき、Derivedのコンストラクタによる初期化が行われる前なのでmsgには初期化子で設定された値が入っている。

問題点
コンストラクタで仮想メソッドを呼び出すようにすると、 派生クラスのコンストラクタで設定される前の値が使われる。 派生クラスにおけるすべてのメンバ変数は初期化子あるいはシステムによって設定された初期状態のまま。

この不安定さを回避するには、派生クラスではすべてのインスタンス変数を初期化子で初期化する、派生クラスではデフォルトコンスタラクタを定義しそれ以外のコンストラクタを作成させない といった制限が必要になる。

C# LINQ Whereの戻り値メモ

やりたかったこと

LINQとnull条件演算子を合わせ、要素がなかったらnullを返してほしい。次の式でemp=nullになると期待。

>var lst = new List<int>();
>var emp = lst.Where(e => e > 0)?.Min();
シーケンスに要素が含まれていません
  + System.Linq.Enumerable.Min(IEnumerable<int>)

nullとはならずSystem.InvalidOperationExceptionが投げられました。

期待通りの結果を得る

まずは期待通りの結果を得る方法。

> var emp = lst.Where(e => e > 0).Min(e => (int?)e);
> emp
null

Minでnull許容型に変換すると要素がない場合nullが帰ってきます。

勘違い

Whereの結果、空ならnullと思っていましたがWhereListIteratorですね。

> var lst = new List<int>();
> var emp = lst.Where(e => e > 0);
> emp.GetType()
[System.Linq.Enumerable+WhereListIterator`1[System.Int32]]
class WhereListIterator<TSource> : Iterator<TSource>
abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>

referencesource.microsoft.com

docs.microsoft.com

ドキュメントにも戻り値はIEnumerableであると書いてありますしね。勉強不足でした。遅延実行などについても詳細は把握してませんし。ぜんぜんわからない。俺は雰囲気でLINQをやっている。

その他適当に調べたこと

int?型のタイプを調べるときはGetType()を使わない。
> int? a = 0;
> a.GetType()
[System.Int32]

値が入ってるとint型

> int? a = null;
> a.GetType()
オブジェクト参照がオブジェクト インスタンスに設定されていません。
  + object.GetType()

nullだと例外。

Null許容型を識別する方法

docs.microsoft.com

配列のリスト
> var lst = new List<int[]>();
> lst.Where(e => e[0] > 0)
Enumerable.WhereListIterator<int[]> { }
> lst.Where(e => e[0] > 0).Min(e => e[0])
シーケンスに要素が含まれていません
  + System.Linq.Enumerable.Min(IEnumerable<int>)
> lst.Where(e => e[0] > 0).Min(e => e?[0])
null
DefaultIfEmpty

シーケンスが空の場合設定した値、設定しない場合規定値を持つシングルトンコレクションを返す。

docs.microsoft.com

> var lst = new List<int>();
> lst.Where(e=>e>0).DefaultIfEmpty()
DefaultIfEmptyIterator { 0 }
> lst.Where(e => e > 0).DefaultIfEmpty(5)
DefaultIfEmptyIterator { 5 }
> var lst = new List<int[]>();
> lst.Where(e => e[0] > 0).DefaultIfEmpty()
DefaultIfEmptyIterator { null }
> lst.Where(e=>e[0]>0).DefaultIfEmpty().Min()
null
> lst.Where(e => e[0] > 0).DefaultIfEmpty().Min(e=>e[0])
オブジェクト参照がオブジェクト インスタンスに設定されていません。
  + System.Linq.Enumerable.WhereSelectEnumerableIterator<TSource, TResult>.MoveNext()
  + System.Linq.Enumerable.Min(IEnumerable<int>)
  + System.Linq.Enumerable.Min<TSource>(IEnumerable<TSource>, Func<TSource, int>)
> lst.Where(e => e[0] > 0).DefaultIfEmpty().Min(e => e?[0])
null
> lst.Where(e => e[0] > 0).DefaultIfEmpty(new int[] { 1, 2 })
DefaultIfEmptyIterator { int[2] { 1, 2 } }

所感

今回の風古戦場やばない?ヤバババババムートじゃない?

#項目15 不必要なオブジェクトの生成を避けること

ガベージコレクタによるメモリ管理にはコストがかかる。 GCの実行条件はメモリの確保量と確保の頻度で決まる。メモリを確保すればするほどより頻繁にGCが実行され、非効率的。 参照型のオブジェクトを大量に使用すれば、アプリケーションのパフォーマンスに大きな影響を与える。

したがって、ヒープベースのオブジェクトの確保・破棄を最小限にして、ガベージコレクタにできるだけ仕事をさせないようにするべき。

ガベージコレクタが行う仕事を最小限に抑えるテクニック

その1 

頻繁に生成・破棄する参照型のローカル変数があればその変数をメンバ変数へ昇格させる。

悪い例

Windowsの描画イベントハンドラにおいてGDIオブジェクトを生成するコード。OnPaintは非常に頻繁に呼び出される。ここで同じ設定のオブジェクトMyFontを毎回生成している。

protected override void OnPaint(PaintEventArgs e )
{
    using(Font MyFont = new Font("Arial", 10f));//毎回同じ設定でFontオブジェクト作成
    {略}
}

MyFontをメンバ変数に昇格させるとよい。毎回の描画のたびにガベージが作られないようになり、ガベージコレクタの仕事量が減る。

private readonly Font MyFont = new Font("Arial", 10f));
protected override void OnPaint(PaintEventArgs e )
{
    略
}
注意点

IDisposableインターフェイスを実装するオブジェクトをローカル変数からメンバ変数へ昇格させる場合、クラス自身にもIDisposableインターフェイスを実装すること(実装方法は項目17を参照)

その2

プログラム中、様々な場所から使用される参照型のインスタンスは、staticメンバ変数にする。

Brushオブジェクト。 プログラム中で多くのウィンドウやコントロールが生成されると、Brushオブジェクトも大量に生成される。そのためBrushオブジェクトを型のメンバ変数へ昇格しただけでは不十分。

.Net FrameworkではBrushesクラスにBrushオブジェクトのstaticメンバを用意している。要求された色のブラシについてのみインスタンスを生成・保持する遅延評価アルゴリズムを採用している。再度その色のブラシが必要になった場合には、保持しているブラシを再利用する。

praivate static Brush blackBrush;
public static Brush Black
{
    get
    {
        if(blackBrush == null)//要求されたときに生成
            blackBrush = new SolidBrush(Color.Black);
        return blackBrush;//以後は生成したブラシを再利用
    }
}

欠点

オブジェクトが不必要に長期間メモリ上に存在することがある。 Disopose()メソッドがいつ呼ばれるのか把握できず、非マネージリソースを破棄できないため。

その3

不変型に対して、それをサポートする可変なビルダクラスを用意する。

悪い例

System.Stringクラスは不変型で、文字列を作成した後はその文字列オブジェクトを変更できない。文字列の内容を変更するようなコードを書いたとしても、実際には新しい文字列オブジェクトが生成され、古いオブジェクトはガベージになる。

string msg = "Hello,";
msg += thisUser.Name;//新しいオブジェクト生成。"Hello,"はガベージに。
msg += ".Today is";//新しいオブジェクト生成。"Hello,<user>"はガベージに
msg += System.DateTime.Now.ToString();//新しいオブジェクト生成。"Hello,<user>.Today is"はガベージに

単純な場合であれば補間文字列を使用、より複雑な文字列操作が必要な倍はStringBuilderクラスを使用する。

StringBuilderは可変な文字列オブジェクトで、不変な文字列オブジェクトを生成できる。StringBuilderで文字列データを生成してから不変な文字列オブジェクトを生成すればよい。

不変型が必要な場合、最終的な不変型のオブジェクトを生成するよりも前に、さまざまな方法で型の情報を変更することが可能なビルダオブジェクトを用意するとよい。

EFFECTIVE C# 6.0/7.0 読書メモ 項目14

項目14 初期化ロジックの重複を最小化する

コンストラクタの定義方法について

推奨方法

共通処理を行うためのコンストラクタを用意する

コードの重複を避けることができ、コンストラクタ初期化子に対応するオブジェクトコードがより効率的なものとして生成される。

C#コンパイラはコンストラクタ初期化子を特別な文法とみなし、重複した初期化変数を削除したり、重複した親クラスのコンストラクタ呼び出しを削除したりする。

コンストラクタ初期化子では、別のコンストラクタを1回だけ呼び出すことができる。

public MyClass():
     this(0, "")//コンストラクタ初期化子。MyClass(int initialCount, string name)が呼ばれる
{}
public MyClass(int initialCount, string name)
{}

デフォルト引数を使う

コンストラクタの重複コードを最小化できる。

デフォルト引数を使うと引数に対するデフォルト値を指定することができ、複数のコンストラクタを1つのコンストラクタに置き換えることができる。

public MyClass(int initialCount):
    this(iintialCount, string.Empty)
{}
public MyClass(string name):
    this(0, name)
{}
public MyClass(int initialCount, string name)
{}

public MyClass(int initialCount = 0, string name ="")
{}

例えば引数4個ある場合、各引数に対して入力する、しないなどを考えるとオーバーロードの数は24通り。 デフォルト引数を使うと1つですむ。

注意点
  • new制約(where T : new())があるジェネリッククラスやメソッドと連携するなら引数なしのコンストラクタを用意しておく必要がある。
  • 引数のデフォルト値はコンパイル時定数。string.Emptyはstringクラスのstaticプロパティであり、コンパイル時定数ではないため使用できない。

  • デフォルト引数を使用すると、引数の名前とそのデフォルト値がpublicインターフェイスの一部となる(名前付き引数?)。このため、引数の名前を変更した場合、このクラスを使用するすべてのコードが名前の変更に追従しないといけない。将来起こり得る変更に対しては、オーバーロードされたコンストラクタの方が耐性が高い。

非推奨

最初のコンストラクタを記述し、コピペして必要な分だけコンストラクタをオーバーライドする。

論外

共通する処理をprivateへルパメソッドに切り出す

推奨方法とは異なり重複した処理(オブジェクト初期化子用の文、親クラスのコンストラクタ呼び出し)が削除されない。

コンストラクタではないのでreadonlyの初期化ができない。

最後に

型のインスタンスが初期化される際に発生する処理を把握しておくこと。

  1. static変数のメモリストレージが0に初期化される
  2. static変数の初期化子が実行される
  3. 親クラスのstaticコンストラクタが実行される
  4. staticコンストラクタが実行される
  5. インスタンス変数のメモリストレージが0に初期化される
  6. インスタンス変数の初期化子が実行される
  7. 適切な親クラスのコンストラクタが実行される
  8. インスタンスコンストラクタが実行される

・すべての変数が意図した通りの値に初期化され、かつ初期化処理が1回だけ行われるようなコードを作成せよ

トラブルメモ CX-Compolet/SYSMAC Gatewayのインストール

CX-Compolet/SYSMAC Gateway のインストールでいろいろ面倒なこと*1が起きたのでメモ

やろうとしたこと

Windows10にCX-Complet/SYSMAC Gateway 1.5.0*2をインストール、PLCとタグデータリンク通信。

まとめ

結論から

経緯

CX-Complet/SYSMAC Gatewayのインストール完了まで

インストールから現象発生まで

  1. Sysmac Studio、CX-Compolet/Sysmac Gatewayをインストール。インストール自体はできたが、「古いバージョンがあるのでWinPCap4.1.3をインストールできない」とのメッセージが出た。
  2. dll(wpcap.dll、packet.dll)の名称を変更してWinPCap4.1.3をインストール。「an error occurred while installing the NPF driver」とのメッセージが出た。インストールはできたことになっていた*5
  3. SYSMAC Gateway Consoleを立ち上げたときにエラー発生。
    ・「NameSpaceServer サービスを、次のエラーが原因で開始できませんでした: 指定されたファイルが見つかりません。」
    ・SYSMAC Gateway の通信サービス(CIPCore)立ち上げに失敗(タイムアウトエラー)
    ・SYSMAC Gateway Consoleからタグテーブルの編集ができない

対応

セーフモードでWinPCap4.1.3をいれなおすと、問題なくSYSMAC Gateway Consoleを使用できるようになった。
※msconfigを利用してセーフモードにするとBitLockerキーの入力は必要なかった。「トラブルシューティング」からだとBitLockerキーの入力がいるようだった。
※通常モードでWinPCapをアンインストールすると「an error occurred while installing the NPF driver」が出た。アンインストールはできたことになっていた。

タグデータリンクができるようになるまで

発生した現象

  • PC-PLC間の通信自体はできている(Sysmac Studioで接続できる。設定のアップロード/ダウンロードなどもできる)が通信エラーが生じていた。
  • タグデータリンクができていない。「タグモニタ」を使用するとPLCのデータ、SYSMAC Gatewayのデータは確認できたが、PLCの出力データに変更があってもSYSMAC Gatewayの入力データに変更がなかった。逆も。

生じていたエラー

  • 「Network Configurator」で確認
     データリンクステータス:照合異常
     コネクションステータス:コード01:0203(0204だったかも)
     が発生していた。
  • 「トラブルシュート」で確認
     入力は正常だが、出力は「切断」になってた

対応

  • 通信の負荷を減らした(データのサイズを小さく、通信周期を長く、タイムアウト値を大きくした)
     エラーの内容で該当しそうなのが「コネクションリソース」だったため。
     →問題解決せず。
  • Windows Defenderのファイアウォールにルールを追加した
     マニュアルにファイアウォールの設定が必要と書いてあったため*6
     →問題解決せず
  • ファイアウォールを無効にした  入力はエラーなし、出力は切断となってればなんかいらん事してる可能性が高いと思った*7
     →問題解決

アプリから通信できるようになるまで

現象

C#にてVaraible Compoletの使用して通信しようとしたところ、実行時にエラー(追加情報:DLL 'EvtMem32.dll' を読み込めません)が生じた。

対応

 プラットフォームターゲットをx86としてビルド
※資料はよく読もう

所感

知ってる人がやりゃあ30分もかからないことに延々と時間を浪費するのは大変苦痛でありスケジュールにも優しくなく私は悲しい。同じ現象が他の人に起こる可能性は限りなく低い気がするけど資料に残しておきます。

*1:自業自得な気しかしない

*2:そもそも公式にはWindow10に対応してない。対応してるバージョン使えば良いと思います

*3:面倒なら無効にしておく。した。外部と繋がってないから問題ねーはずでーす

*4:そもそも動作環境に「アプリケーションは32bitプロセスとして動作させる必要があります」と書いてあるし。ちゃんと読もう

*5:「アプリと機能」の一覧に追加されていた

*6:Win7までの方法。Win10については書いてなかった

*7:他に「システム管理者による設定」が複数ありポイント倍点