20190503のC#に関する記事は4件です。

【Unity】簡単かもしれないTextだけで選択メニュー

はじめに

Textだけで選択メニュー的なのを作ってみました。デザインをこったりすることはできませんが、簡単なUI作るのになら使えそうです。

コード

Sentaku_Text.cs
    public int SentakuID = 0;
    int cnt = 0;

    Text _text;

    void Start(){
        _text = GetComponent<Text>();
    }

    void Update(){
        float YAxis = Input.GetAxisRaw("Vertical");

        //長押しで連続入力
        if (YAxis != 0) {
            cnt++;
            if (cnt == 1) SentakuID -= (int)YAxis;
            else if (cnt >= 20) SentakuID -= (int)YAxis;
        } else if (YAxis == 0) cnt = 0;

        //一番端にいったら反転
        if (SentakuID > 2) SentakuID = 0;
        if (SentakuID < 0) SentakuID = 2;

        Sentaku();
    }

    void Sentaku() {
        switch (SentakuID) {
            case 0:
                _text.text = ">0だよ<" + "\n" + "1だよ" + "\n" + "2だよ";
                break;

            case 1:
                _text.text = "0だよ" + "\n" + ">1だよ<" + "\n" + "2だよ";
                break;

            case 2:
                _text.text = "0だよ" + "\n" + "1だよ" + "\n" + ">2だよ<";
                break;

            default:
                break;
        }
    }

条件分岐の処理で該当する部分を"><"で挟むことで、選ばれてる感を出してます。
ちょっと説明しづらいので動作の様子は以下の通りです。
Sentaku_Text.gif

今回は処理で書いてないですが、非表示にしたいときは空文字をいれれば大丈夫です。

終わり

問題は選択肢が増えてきたときに、文字の入力がかなり面倒なことになりそうなところです。
でもちょろっと使う分には結構楽だと思います。選択肢が多いときは別の方法でやったがいい気がします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

object x = 1 の時 x == 1 が真にならない理由

クイズ

まず始めに、このコードの実行結果がどうなるか、考えてみてほしい。

例1
object x = 1;
Console.WriteLine(x == 1 ? "OK" : "NG");

出力結果は、以下の通り。

例1の出力結果
NG

なぜこうなるのか、あなたは正確に説明できるだろうか?

三項演算子がおかしいとか、記載されていないコードがあって変数が書き換えられているとか、そういう話ではない。

このコードの x == 1 の判定は、間違いなく偽になっており、そしてそれは言語仕様通りだ。

ヒント

以下のように書き換えると結果が変わる。

例2
object x = 1;
Console.WriteLine(x.Equal(1) ? "OK" : "NG");  // OK

== の代わりに Equal() を使うと結果が変わる。

面白いのは、次の例だ。

例3
dynamic x = 1;
Console.WriteLine(x == 1 ? "OK" : "NG");  // OK

例1と違うのは、変数の型が object ではなく dynamic になっているという点だ。

次はちょっと難しくなるが、どうなるだろうか。

例4
void Func<T>(T x) {
  Console.WriteLine(x == 1 ? "OK" : "NG");
}
Func(1);  // OK

例1 と同じく == を使っているが、なぜ結果が異なるのだろうか。

なぜこのような結果になるのか、ちゃんと説明できる人はどれぐらいいるだろうか?

理解のポイントは、型の評価のタイミングだ。

実は C# では コンパイル時の型実行時の型 という2種類の解釈があるのだ。

解説

原因は、出力が "NG" になるケースでは object クラスの == 演算子が呼び出されているためだ。
特に C# では string の比較に == を使うのが一般的であるため、直感的でない挙動になる場合について知っておく必要がある。

実務でもたまに遭遇する ハマりポイント でもある。
例えば WindowsForms でデータグリッドビューを使ったことがあるだろうか。セルの Value が object なので、==stringint と比較しようとしても全く True にならないので、僕は小一時間ほどハマったことがある(笑

また、自作クラスで演算子をオーバーロードした時にも注意が必要だ。つまり、どのクラスの演算子が呼び出されるかは、実行時の型ではなく、コンパイル時の型によって決定される。

C# では、演算子のオーバーライドが出来ないようになっており、演算子をオーバーロードする際は static として宣言しなくてはならず、virtual 指定することも出来ない。従って演算子は実行時の型による動的ディスパッチ1が出来ないのだ(ただし出来る場合もある、後述)。

そもそも C# の世界に「演算子のオーバーライド」という概念はない。あるのは「メソッドの オーバーライド 」と「演算子の オーバーロード 」だ2

その辺からキッチリ理解していこう。

オーバーライドとオーバーロードの違い

まずはオーバーライドとオーバーロードの違いをザックリおさらい。

オーバーロード

オーバーロード (overload) とは、「 多重定義 」のことだ。メソッド名が同じで引数リストが異なるメソッドを作ることだ。

オーバーロードの例
void Exec(string arg)
{
  Exec(arg, 60);
}

// 同じ名前で引数の異なるバージョンを多重に定義。これがオーバーロード
void Exec(string arg, int timeout)
{
  ...
}

このように同じ名前のメソッドを多重に定義することを「オーバーロード」と言う。

オーバーロードされたメソッドのうちどれを呼び出すかは、コンパイラによって決定される。

オーバーライド

一方オーバーライド (override) は、クラス継承の際にメソッド名も引数リストも全く同じメソッドを定義し、基底クラスのメソッドを「 上書き 」することを指す。

C# では、virtualoverride キーワードを用いる必要がある。

オーバーライドの例
class ClassA
{
    public virtual void Exec(string arg)
    {
    }
}

// ClassA を継承したクラス
class ClassB : ClassA
{
    // 基底クラスのメソッドを上書き。これがオーバーライド
    public override void Exec(string arg)
    {
    }
}

コンパイラは、virtual 指定されたメソッドの呼び出しがあると、コンパイル段階ではどのクラスのメソッドが呼び出されるか決定されない。というか決定することが出来ない。実行時でないとどの型なのか分からない ためだ。

ClassA c = GetInstance();
c.Exec(arg);   // cはClassBのインスタンスかもしれない。コンパイル時には分からない。

virtual 指定がないメソッドのオーバーライド

ところで、virtual 指定がないメソッドに対してサブクラスで「上書き」しようとすると、new を付けると上書き可能になるが、実際それを試してみるとオーバーライドではなく、再定義になる。

従って、変数の型でどのメソッドを呼び出すかが決定される。

例: new で再定義すると変数の型(コンパイル時の型)で呼び出すメソッドが決まる。

newで再定義した場合
class ClassA
{
    public void Exec()
    {
        Console.WriteLine("A");
    }
}

// ClassA を継承したクラス
class ClassB : ClassA
{
    // 基底クラスのメソッドをオーバーライド?ではなく再定義になる
    public new void Exec()
    {
        Console.WriteLine("B");
    }
}

class Program
{
    static void Main(string[] args)
    {
        ClassB b = new ClassB();
        b.Exec();  // B

        ClassA c = b;
        c.Exec();  // A  <= 値の型は ClassB であるが、変数の型が ClassA であるため
    }
}

このように、C#では virtual 指定がないメソッドをオーバーライドすることは物理的に不可能だ。理由は virtual 指定がないと、実行時に動的ディスパッチするために必要な「仮想関数テーブル3」という内部データが作られないためだ。

演算子のオーバーロード

さてそろそろ本題の演算子のオーバーロードの話に入ろう。

まずは演算子のオーバーロードの例を見てみよう。

演算子のオーバーロード
class ClassA
{
    public string Value1 { get; protected set; }

    // コンストラクタ
    public ClassA(string val) { Value1 = val; }

    // == 演算子を定義
    public static bool operator==(ClassA self, ClassA other)
    {
        return self.Value1 == other.Value1;
    }
    // == を定義するためには != もペアで定義が必要
    public static bool operator!=(ClassA self, ClassA other)
    {
        return !(self == other);
    }
}

このようになる。

ここでは operator==() を1つしか定義していないが、それでも「演算子をオーバーロードしている」状態になる。

なお、operator!=() の定義も必要なのは C# の仕様だ。中身は == の否定をとるだけでオッケーで、これが定石だ。これでコンパイルが通るが、実務で == をオーバーロードするのは注意が必要だ。多くの場合、Equals() をオーバーライドするのが適切だ。ここでは例示のための実験的なコードとして捉えておいてほしい4

ではこれを継承したクラスを作り、オーバーライド してみよう。

演算子のオーバーライド?
// ClassA を継承したクラス
class ClassB : ClassA
{
    public int Value2 { get; protected set; }

    public ClassB(string val1, int val2) : base(val1) { Value2 = val2; }

    // == をオーバーライド?(実は出来ていない)
    public static bool operator==(ClassB self, ClassB other)
    {
        return self.Value1 == other.Value1 && self.Value2 == other.Value2;
    }
    // == を定義するためには != もペアで定義が必要
    public static bool operator!=(ClassB self, ClassB other)
    {
        return !(self == other);
    }
}

試しに上記クラスを利用するコードを書いて確かめてみよう。

オーバーライド?した演算子を呼び出す
class Program
{
    static void Main(string[] args)
    {
        // ClassA のインスタンスを比較
        var a1 = new ClassA("A");
        var a2 = new ClassA("A");

        Console.WriteLine(a1 == a2);  // True

        // ClassB のインスタンスを比較
        var b1 = new ClassB("1", 10);
        var b2 = new ClassB("1", 20);

        Console.WriteLine(b1 == b2);  // False  <= Value2 の値が異なるので False

        // ClassA 型の変数に代入(キャスト)
        ClassA c1 = b1;
        ClassA c2 = b2;

        Console.WriteLine(c1 == c2);  // True  <= ここで、ClassAの==演算子が呼ばれている

        Console.WriteLine($"{c1.GetType().Name}, {c2.GetType().Name}");  // ClassB, ClassB  <= 実体はどちらもClassB
    }
}

案の定、これでは想定どおりに動かない。static だし、virtual 指定ができないし、基底クラスの == とは引数リストも異なるので、全くオーバーライドになっていない。完全に異なる別々のメソッドになっている。

冒頭で説明したとおり、C# では 演算子をオーバーライドすることは出来ない のだ。5

コンパイル時の型と実行時の型

これまで見てきたように、C# ではコンパイル時の型と実行時の型を区別する意識が必要だ。
コンパイル時の型とは、言い換えると 変数の型 のことで、実行時の型とは、値の型 (実体)のことだ。

では、演算子は必ずコンパイル時の型で評価されるのかというと、そうでもない。

dynamic を使う場合

C# の dynamic は実行時に該当する型を判別して動的にコードを生成してくれる凄い仕組みだ。
ダックタイピングのような機能を提供する。

dynamic として宣言された変数はその名前のごとく動的(つまりコンパイル時ではなく実行時)に、型が解決される6。実行時というか、なので、例3の場合だと int 型で実行時にコード展開され、実行される。

デメリットとして、コンパイル時に型情報が一切ないので、インテリセンスも利かないし、何より静的型付け言語の最大のメリット「タイプセーフ」でなくなってしまう。
乱用は禁物だ7
ここではこれ以上の詳細な説明は省略するが、興味があれば調べてみると良いだろう。

ジェネリックを使う場合

クラスやメソッドにジェネリックを使って任意の型に適用出来るようにすると、コンパイル時の型に解決される。

冒頭の例4では、型パラメータ <T>int として解釈されるため、int== 演算子が呼ばれるようになる。

タイプセーフであるため、普通は dynamic よりもこの仕組を使うべきだが、あくまでコンパイル時の型で評価されるため、万能ではない。
実際、例4に以下のように追記すると、例1と同じ結果になってしまう。

例5
void Func<T>(T x) {
  Console.WriteLine(x == 1 ? "OK" : "NG");
}
Func(1);  // OK
object obj = 1;
Func(obj);  // NG  <= T が object として評価されるため

まとめ

「コンパイル時の型」と「実行時の型」という2つの解釈があることを認識しよう。

  • オーバーライドされたメソッドは、実行時の型でディスパッチされる
  • オーバーロードされたメソッドは(演算子に限らず全て)、コンパイル時の型でディスパッチされる
  • ただし dynamic を使った場合は実行時にコンパイルされるため、オーバーロードされたメソッドでも実行時の型でディスパッチされることになる

3つ目はエンタープライズ開発の現場ではほとんどお目にかかる事はないと思うので、例外的パターンぐらいの捉え方で十分と思う。

Java と比較すると、C# では文字列の比較にも == が使えるため、普段から Equal() メソッドを使うことはとても少ないと思われる8

しかしながら変数の型が object ならば、== は想定どおりに動かない、ということを知っておくべきだ9

特に Java から C# に来た人は、「string が == で比較できるので直感的でいいな~」 なんて安易に捉えているハマることになります。


  1. ディスパッチ: 複数の関数の中から一定の規則によって呼び出すべき関数を引き当てて呼び出すこと 

  2. 他のプログラミング言語では、演算子をオーバーライド可能なものもある。例えば Ruby では演算子は一般的なメソッド呼び出しの糖衣構文という言語仕様になっているため、オーバーライド可能だ。 

  3. 仮想関数テーブル (V-Table) とは C++ から導入された、コンパイル結果に含められる内部データのことで、クラスの継承ツリーをたどってオーバーライドされたメソッドを実行時に引き当てる仕組みのためのものだ。通常、プログラマが意識する必要はない。オーバライドされていようがされていまいが、メソッド呼び出し時には仮想関数テーブルの処理が行われるため、負荷が生じる。この負荷を避けるためにデフォルトでは仮想関数テーブルが作られず、オーバライドするためには virtual 指定が明示的に必要という言語仕様を採用している。これは C++ から C# にそのまま引き継がれた考え方だ。 

  4. ここでは本筋とはズレるので詳細説明は省略するが == をオーバーライドする場合は無限ループに陥りやすいので注意が必要だ。また Equals()GetHashCode() もオーバライドしておくべきだ。詳しくはMSDN Equals() と演算子 == のオーバーロードに関するガイドライン (C# プログラミング ガイド) および 方法: 型の値の等価性を定義する などを参照のこと。 

  5. C# の static メソッドは、インスタンスに所属しておらず、そのクラスに名前空間的に配置される孤立したメソッドというような捉え方をすべきだ。演算子のオーバーロードを static で書くルールになっている以上、どうあがいてもオーバーライドすることは出来ない。 

  6. 実行時に型を解決する仕組みとして、実行時にコード生成してコンパイルして実行しているような仕組みになっている。コンパイル結果に動的コード生成のためのコードが含まれるようになるためバイナリサイズが大きくなるし、初回実行時にそれなりのオーバヘッドがかかる。 

  7. 例えば Dictionary<string, object> を使って文字列をキーにした連想配列で大きなデータ構造を扱うようなコードを書くぐらいなら、そもそもタイプセーフもクソもないので、ExpandoObject を使う方がよっぽどマシかもしれない。 

  8. 例えば Java では演算子のオーバーロードが言語仕様として存在せず、== は単にリファレンスの比較(ポインタの比較)となる。異なる型を == で比較しようとするとコンパイルエラーとなる。 

  9. 実際の開発現場でよくあるのは、データグリッドビューにセットしたオブジェクトと == で比較してしまって想定通りに動かない、というパターンだ。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

和暦年から西暦年に変換

元号が令和に代わるとき、システム改修を行った際に考慮したメモです。

要件

和暦の元号と年から西暦年へ変換したいことはよくあるかと思います。
例えば、自分の誕生日を年月日バラバラで入力する項目などですね。

本来、元号が変わる際は月日も考慮する必要があります。
例えば、西暦2019年は二つの元号を考慮する必要がありますが、月日まで指定されると、元号は一つに絞られます。

  • 平成31年 4月 30日 ⇔ 2019年 4月 30日
  • 令和元年 5月 1日 ⇔ 2019年 5月 1日

ただ、上記からわかる通り、和暦年だけからでも西暦年に変換することは可能です。

≪今回の要件前提≫

  • 和暦の元号と年のみから西暦年を取得したい。
  • 和暦の元号は、近代の明治以降のみ考慮する。
  • 和暦の元号の末年以上の年を入力されても、エラーとせず西暦に変換したい。
    例えば、平成32年は存在しないが2020年として取得したい。
  • タプルを事前に用意せず、システムのデータを利用する。
    今回私が対応した案件は Windows アプリ(C#)なので、Windows(.NET Framework)のシステムデータを使い、和暦年から西暦年へ変換する。

結論を先に

C# で和暦から西暦へ変換する際には、年だけでなく月日のデータが必要です。
(他の言語でも、基本は年月日のすべてが必要と思います。)

変換に利用する仮の月日は、12月31日 を指定する方法がスマートです。

using System.Globalization;

string era;   // 元号の文字列
int eraYear;  // 和暦の年

CultureInfo culture = new CultureInfo("ja-JP", true);
culture.DateTimeFormat.Calendar = new JapaneseCalendar();

string warekiDate = era + eraYear.ToString() + "/12/31";
DateTime date = DateTime.Parse(warekiDate, culture.DateTimeFormat);

int year = date.Year;   // 西暦の年

なぜ12月31日を指定するのか

分かりやすさで言えば、1月1日でもよい気がします。
これには2つ理由があります。

【明治対応のため】

元号は1月1日など固定された日から変わるものではなく、ある日を境に突然変わるものです。
当然、明治元年も1月1日からではありません。
(当時は1月1日からみたいな内容もありましたが、話がずれるため Wikipedia なんかで調べてもらえればと思います。)

明治元年は現在の暦上で、1868年1月25日から始まります。(Wikipedia 調べ)
また .NET Framewrok 上では、1868年9月8日からのみ明治の日付データとして受け入れ可能となります。

上記のプログラムで1月1日を指定していた場合、明治元年がエラーで変換できなくなります。

【改元年に元号が確実に変更されている日であるため】

そもそもの、元号と年について考えてみます。

  • 新元号に変わる日は、だれにもわからない。
  • 新元号元年(新元号1年)から新元号2年に変わる日は、新元号元年の翌年の1月1日である。

上記をもとに、新しい元号に変わったケースをいろいろ考えてみます。
テストケースは、年末年始及び過去の改元日です。

改元日 年始 旧元号末日 新元号初日 年末
1月 1日 新元号年 1月 1日 (前年の 12月 31日) 新元号年 1月 1日 新元号年 12月 31日
1月 8日 旧元号年 1月 1日 旧元号年 1月 7日 新元号年 1月 8日 新元号年 12月 31日
1月 25日 旧元号年 1月 1日 旧元号年 1月 24日 新元号年 1月 25日 新元号年 12月 31日
5月 1日 旧元号年 1月 1日 旧元号年 4月 30日 新元号年 5月 1日 新元号年 12月 31日
7月 30日 旧元号年 1月 1日 旧元号年 7月 29日 新元号年 7月 30日 新元号年 12月 31日
12月 25日 旧元号年 1月 1日 旧元号年 12月 24日 新元号年 12月 25日 新元号年 12月 31日
12月 31日 旧元号年 1月 1日 旧元号年 12月 30日 新元号年 12月 31日 新元号年 12月 31日

つまり、元号が変わった年の12月31日は必ず新元号であることが確実なのです。
逆を言うとそれ以外の日は、旧元号の日付であるか、新元号の日付であるかは、その改元が訪れるまで分からないということになります。
その都度、改修やテストが必要となることは避けておきたいですよね。

新元号の元年を西暦の年に直したい場合、12月31日で変換するとエラーとならずに確実なのです。


考えられる追加要件

今回の案件は上記までの対応でOKだったため、追加の考慮は必要なかったのですがメモとして記載しておきます。

元号の末年以上の年を許容しない

例えば、平成32年は存在しないのでエラーとしたい、もしくは正しい和暦に変換したい等の「あるある」要件です。

方法としては、以下の2つが考えられます。

≪前提≫

  • 和暦を年だけで判定する場合は、改元前の末年と改元後の元年は同じ年である
  • 和暦を年月日で判定する場合は、月日に応じて改元前と改元後の元号と和暦年を考慮する

和暦年⇔西暦年の再変換を利用

和暦年→西暦年→和暦年と再変換させるやり方です。
1. 上記のロジックを使用して和暦年→西暦年に変換
2. 再度、西暦年から和暦年に変換
3. 上記の和暦が等しいか判定
4. (月日を考慮しない場合)再変換後の和暦が元年だった場合、改元前の末年と等しいか判定する処理が追加で必要

利点

  • OSやライブラリの仕様に準ずることができるため、後々手を入れる必要がない。(と思いたい。)
  • 西暦から和暦に再変換した値を再利用できたりなんかするといいなぁ。

欠点

  • サポート切れのOSなんかを使用されている顧客に対応しづらい。
  • 演算コストがかかるので、大量のデータ処理には不向き。

元号と西暦のタプルを用意

個人的にはおススメしない方法です。
元号が始まった日を、元号と組み合わせてマスターデータのように保持させます。
例えば、『令和』であれば、『2019-05-01』をタプルとして保持します。

  1. 和暦元年の年月日データを事前に準備
    • {明治, 1868/1/25}
    • {大正, 1912/7/30}
    • {昭和, 1926/12/25}
    • {平成, 1989/1/8}
    • {令和, 2019/5/1}
  2. 上記のロジックを使用して和暦年→西暦年に変換
  3. 入力値とタプルの日付を比較
  4. (月日を考慮しない場合)再変換後の和暦が元年だった場合、改元前の末年と等しいか判定する処理が追加で必要

利点

  • OSやライブラリの仕様に非依存のため、移植がかんたん。
  • OSやライブライ非依存のため、サポート終了後も対応可能。

欠点

  • 元号が増えるたびに、追加作業や適用作業が必要。
  • 作り方によっては、作者しか仕様を把握していない。
  • つまり、自分で責任もってメンテしなければならない。(大問題)

最後に

これは、新人教育の良い例題だなーと思いつつ書きました。
考慮漏れ等指摘があれば、よろしくお願いします!

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web リクエストがエラーになったときのレスポンスボディを取得する (.NET)

Web リクエストがエラーになったときのレスポンスボディを取得する (.NET)

特に Web API 呼び出しだと、エラー応答に修正に重要な情報が載っていることが多いので.
例外に Response オブジェクトが入っていて、後は正常時と同様に GetResponseStream を呼べば OK.

catch (WebException e)
{
    using (var rs = new StreamReader(e.Response.GetResponseStream())) {
        Console.WriteLine(rs.ReadToEnd());
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む