Open6

独習C# 第5版

しみゆーしみゆー

Mainメソッド

  • C#ではアプリ起動時、一番最初にMainメソッドを探して実行する(エントリーポイント)
しみゆーしみゆー

名前の解決

  • クラスは、「名前空間 + クラス名」で識別できる
  • 名前空間も含めて記述された名前のことを"完全修飾名"と呼ぶ
しみゆーしみゆー

.NET環境

大前提

コンパイルされた後のマシン語は、プラットフォーム(windows,Linuxなど)に固有のもの。異なるプラットフォームでは動かない

C#では

C#では、マシン語にコンパイルされるのではなく、CIL(共通中間言語)として出力される。CILは、.NET環境が解釈できるものとなっている。.NET環境は、Windowsをはじめ様々なプラットフォームに応じて提供されている。.NET環境がプラットフォーム間の差分を吸収することでC#がマルチプラットフォームで動作できる。

.NETの種類

.NET Framework

  • 元祖.NET
  • Windowsに限定された動作環境でマルチプラットフォームに対応していない

.NET Core / .NET

  • マルチプラットフォームに対応
  • バージョン5で.NETに名称が変わった
  • 現在主に使われるのは.NET
しみゆーしみゆー

Visual Studioにおけるソリューションとプロジェクト

  • メインのプログラム(.exeファイル)とメインプログラムから利用するライブラリ(.dllファイル)が存在する場合、それらを別々のプロジェクトに分けるのが一般的
  • これらプロジェクトをさらに意味のある単位でまとめるものとしてソリューションがある。
    • 1つのソリューション内に1つのプロジェクトという単純なアプリケーションもある

アセンブリ

ビルドによって作成される実行可能ファイル(.exe,.dll)
Visual Studioでは、1プロジェクト1アセンブリとなる。

しみゆーしみゆー

フィールド

  • メンバー変数。class内で定義される変数のこと
  • 原則としてprivateであるべき
    • camelCase
    • 先頭に'_'を付けることが多い(プロジェクトの規則によるが)
  • 外部から直接操作できないようにする。変更やアクセスは専用のメソッド等を利用するべき

アクセサーメソッド

フィールドを外部から操作できないようにし、変更やアクセスにおいては専用のメソッドを使うべき。それを実現する方法として、アクセサーメソッドがある。

internal class Triangle
{
  private double width;
  private double height;
  
  public double GetWidth()
  {
    return this.width;
  }
  
  // set***メソッドを省いたフィールドは読み取り専用になる。
  public void SetWidth(double width)
  {
    if (width <= 0) 
    {
      throw new ArgumentException();
    }
    this.width = width
  }
  // ... heightのgetter,setterは省略
  public double GetArea()
  {
    return GetWidth() * GetHeight() / 2;
  }
}
  • フィールドの数だけ、GetとSetのメソッドが増えていく
    • 本来のクラスの役割が見えにくくなる。

プロパティ

C#では、アクセサーメソッドの代わりにプロパティを使う
プロパティは、

  • クラス内ではメソッドのように記述する
  • 外部からはフィールド(変数)のようにアクセスできる
    仕組み。
internal class Triangle
{
  private double _width;
  private double _height;

  public double Width
  {
      set
      {
         if (value <= 0) 
         {
            throw new ArgumentException();
          }
          this._width = value 
       }

       get
       {
          return this._width;
        }
  }

  public double Height
  {
    // ... heightのget,setは省略
  }

  public double GetArea()
  {
    return Width* Height / 2;
  }
}

セッター(setブロック)

  • 値のチェックやセットが役割
  • 戻り値はない
  • setブロックを省略した場合は、読み取り専用となる(不変クラス。イミュータブルクラスとも)
    • 書かなくて済むならsetブロックは省略し、不変クラスとするのが良い

ゲッター(getブロック)

  • フィールドから取得した値を返す

setブロックやgetブロックのメリット

正直、アクセサーメソッドと大して変わらないのでは?とも思える。ただし、下記のメリットがある。

  • メソッドと区別できる
    • アクセサーメソッドと通常のメソッドが混ざっていると可読性が落ちる
  • setブロックとgetブロックがまとまっているので読みやすい
  • 利用側からすると、変数を呼ぶのにメソッド(アクセサーメソッド)を呼ぶのは直感的ではない。getブロックを使えば、利用するときに変数(フィールド)と同様に呼び出すことができる

自動プロパティ

get; set;を渡すと、内部的に値を保存するコード、値を出し入れするコードが自動生成される。
値の取得と設定だけをしたい場合は、この自動プロパティを使うのが良い。

    internal class Person
    {
        public string FirstName { get; set; } = "";
        public string LastName { get; set; } = "";

        public string showFullName()
        {
            return $"{LastName}{FirstName}";
        }
    }

Get-Only自動プロパティ

宣言時、コンストラクタでのみ値をセットできる。
それ以降の値の代入はできなくなる(読み取り専用)

    public string FirstName { get; } = "";

Init-Only

    public string FirstName { get; init; } = "";
  • setの代わりに使う(setとinitを一緒に使うことはできない)
  • 初期化のタイミングでのみ値を設定できる。

Get-OnlyとInit-Onlyの違いは??

Init-Onlyは、Get-Onlyよりも柔軟。
Get-Onlyなプロパティは、オブジェクト初期化子やwith式で値を設定できない。
基本的に、読み取り専用にしたい場合はInit-Onlyを使う

    internal class Person
    {
        public string Name { get; set; } = ""; // set

        public int Age { get; init; } = 0; // init

        public string Address { get; } = ""; // get-only

        public Person(string name = "", int age = 0, string address = "")
        {
            Name = name;
            Age = age;
            Address = address;
        }
    }

    // コンストラクタで値を設定するとき
    // init,get-onlyともに値を設定できる
    var person1 = new Person("山田太郎", 26, "東京都杉並区...");
    Console.WriteLine(person1.Name);
    Console.WriteLine(person1.Age);
    Console.WriteLine(person1.Address);
    
    // オブジェクト初期化子で定義するとき
    // initは設定できるが、get-onlyは設定できない
    var person2 = new Person
    {
        Name = "山田哲人",
        Age = 29,
        Address = "東京都中野区..." // プロパティまたはインデクサ"Person.Address"は読み取り専用であるため、割り当てることできません
    };
    Console.WriteLine(person2.Name);
    Console.WriteLine(person2.Age);
しみゆーしみゆー

レコード型

クラスは、データ(フィールド)とそれに関連するメソッドを集めたもの。
しかし、時にデータの集合だけを取り扱うクラスが出てくる。そのような時に使うのがrecord型。

メリット

  • 定型メンバーの自動生成
  • 既定でイミュータブルなクラスとなる
    • 通常のclassは既定でミュータブル。しかし、データ受け渡しが目的のクラスなのでイミュータブルが望ましい。

使い方

// これをプライマリコンストラクタと呼ぶ
public record Person(string Name, int Age, string Address);

プライマリコンストラクタによって、()に与えたstringのNameプロパティなどを初期化するためのコンストラクタなどが自動生成される。
つまり、以下のコードと同義。

    public record Person
    {
        // 初期化以降は変更できないため、setではなくinit
        public string Name { get; init; }
        public int Age { get; init; }
        public string Address { get; init; }

        public Person2(string name, int age, string address)
        {
            Name = name;
            Age = age;
            Address = address;
        }
    }

その他の特徴

public record Person(string Name, int Age, string Address);

var p1 = new Person("山田太郎", 26, "東京都");
var p2 = new Person("山田太郎", 26, "東京都");

Console.WriteLine(p1); // Person { Name = 山田太郎, Age = 26, Address = 東京都 }
Console.WriteLine(p1 == p2); // True

通常のクラスの場合、==演算子では参照の一致を判定する。
record型の場合は、プロパティが全て一致しているかで同一性を判定する。

record型のクラスを作った時点で、自動的に以下のようなコードが作られるイメージ

    public override bool Equals(object? obj)
    {
        return obj is Person2 person2 &&
            Name == person2.Name &&
            Age == person2.Age &&
            Address == person2.Address
    }

with式

  • record型専用の構文
    • 匿名型、構造体でも使える
  • 現在のインスタンスを複製し、一部のプロパティを書き換えることができる

なぜ必要?

record型は、既定でイミュータブル。
しかし、実際には途中で値を変更したいケースは出てくる。

そこで、

  • 他のインスタンスにも影響を与えない
  • record型のクラス自体をミュータブルにする必要もない
    形で一部プロパティを書き換えるwith式が必要となる。

使い方

public record Person(string Name, int Age, string Address);

var p1 = new Person("山田太郎", 26, "東京都");
var p2 = new Person("山田太郎", 26, "東京都");

// Addressプロパティを書き換える
var p1Copy = p1 with { Address = "神奈川県" };
Console.WriteLine(p1Copy); // Person { Name = 山田太郎, Age = 26, Address = 神奈川県 }

var p2Copy = p2 with { }; // ただのコピー
Console.WriteLine(p2 == p2Copy); // True  複製され中身が等しいのでtrue
Console.WriteLine(ReferenceEquals(p2, p2Copy)); // False  複製された別インスタンスなので、同一インスタンスかどうかを判定する`ReferenceEquals`ではfalseとなる