やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#の概念 プロパティ

 自動実装プロパティは簡潔だけど応用が効かない。だが手動だと冗長すぎて使う気になれない。

成果物

コード

class Main {
    public void Run() {
        var h = new Human("NAME");
        Console.WriteLine($"{h.Name}");
    }
}
class Human {
    public string Name { get; }
    public Human(string name) => Name = name;
}

 setを用意していないのに代入するとエラー。

class Human {
    public string Name { get; }
    public void SetDefault() => Name = "Default"; // error CS0200
}

シリアライズ

public class Human {
    public string Name { get; set; }

    [System.Xml.Serialization.XmlIgnore]
    public int Id { get; set; }

    public Human() {}
    public Human(string name) => Name = name;
}

 シリアル化するためには以下のような制約がある。

  • classpublicにする
  • publicな引数なしコンストラクタを作る
  • プロパティをpublicにしてsetを与える

失敗ログ

 実行時エラー。引数なしコンストラクタが必要。

System.InvalidOperationException: Concept.Property.Lesson3.Human cannot be serialized because it does not have a parameterless constructor.

 実行時エラー。publicでないとダメ。

System.InvalidOperationException: Concept.Property.Lesson3.Human is inaccessible due to its protection level. Only public types can be processed.
public class Human {
    public string Name { get; set; }

    [System.Xml.Serialization.XmlIgnore]
    public int Id { get; set; }

    public Human() {}
    public Human(string name) => Name = name;
}

 出た。けどIdまで出ちゃってる。NonSerialized

<?xml version="1.0"?>
<Human xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Name>NAME</Name>
  <Id>0</Id>
</Human>

 よく見ると[field:NonSerialized]はフィールドに対してのみ有効らしい。ならなぜ公式は以下のようなコードを書いたんだ? プロパティに対して使っている。まるで可能であるかのように…。

[field:NonSerialized]
public int Id { get; set; }

 シリアル化しない方法は以下。XmlIgnore属性を使う。

[System.Xml.Serialization.XmlIgnore]
public int Id { get; set; }

 Humanをシリアライズする例は以下。

class Main {
    public void Run() {
        var h = new Human("NAME");
        Write(h);
    }
    private void Write(Human h) {
        System.Xml.Serialization.XmlSerializer writer =   
            new System.Xml.Serialization.XmlSerializer(h.GetType());  
        using (System.IO.FileStream file = System.IO.File.Create("Human.xml")) {
            writer.Serialize(file, h);  
        }
    }
}

Human.xml

<?xml version="1.0"?>
<Human xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Name>NAME</Name>
</Human>

INotifyPropertyChanged

 値が代入されたときにイベントが発生するヤツ。MVVM。

class Main {
    public void Run() {
        var h = new Human("NAME");
        Console.WriteLine($"{h.Name}");
    }
}
public class Human : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    private void RaiseRenamed([System.Runtime.CompilerServices.CallerMemberName]string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private string name;
    public string Name { get => name;
        set { name = value; RaiseRenamed(); }
    }
    public Human() { PropertyChanged += OnPropertyChanged; }
    public Human(string name) { PropertyChanged += OnPropertyChanged; Name = name; }
    ~Human() { PropertyChanged -= OnPropertyChanged; }
    private void OnPropertyChanged(object sender, PropertyChangedEventArgs args) {
        Console.WriteLine($"値がセットされた!: {args}");
    }
}
値がセットされた!: System.ComponentModel.PropertyChangedEventArgs
NAME

 PropertyChangedメンバは継承せねばならない。名前も固定。変えたらエラー……。

error CS0535: 'Human' does not implement interface member 'INotifyPropertyChanged.PropertyChanged'

妄想

妄想

 上記のコードは汚い。もっと簡潔に書きたい。以下みたいに。残念ながら動作しない。

class Human {
    public string Name {
        get; 
        set {
            Name = value;
            raise;
        }
        raise { // sender, args の引数が使える
            Console.WriteLine($"値がセットされた!: {sender}, {args}");
        }
    }
}

 Nameプロパティがセットされたときのイベントを、クラス内の別の場所で呼び出したいときは以下。

class Human {
    public void Some() {
        raise Name; // PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)))
    }
}

 外部からイベントハンドラを設定するなら以下。

var h = new Human();
h.Changed.Name += (sender, args) => { 独自イベント処理; };

 または以下でも可。

h.Changed["Name"] += (sender, args) => { 独自イベント処理; };

 C#8.0との違いは以下。

  • 自動実装プロパティの改善
    • フィールド宣言不要。プロパティ名Nameをそのまま使って代入可
    • getだけセミコロン;で省略しつつ、set{}で実装できる
  • Invoke略記
    • プロパティ内のraise;キーワードはPropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(プロパティ名)))を意味する
  • イベント時のメソッド定義
    • class内
      • raise プロパティ名(...) => {...}
    • class外
      • human.Changed.Name += (sender, args) => {...}

 さらに短縮したい。+/-(</>)でコードの先頭または最後にraise;するよう指定できたら。

public string Name { get; set+; }
public string Name { get; set-; }
public void Some() raise+ => { ... }
public void Some() raise- => { ... }
public void Some() raise => { ... }
public void Some() + => { ... }
public void Some() - => { ... }

 つまり以下のようにするとNameに値がセットされたとき、raise =>で定義したメソッドが実行される。

public string Name { get; set-; raise => Console.WriteLine($"値がセットされた!: {sender}, {args}");}

 raise;で省略できる。このときはConsole.WriteLine($"Raise: {sender}, {args}");のような処理をデフォルトとする。

public string Name { get; set-; raise; }

 +/-があればraise;も省略できる。

public string Name { get; set-; }

 これだけ縮められたら嬉しいのになぁ。

所感

 プロパティでないところで長くなってしまった。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux