20201215のC#に関する記事は9件です。

float値を文字列にした後、再び数値にするとどれだけずれるか

数値→文字列→数値したときのずれ

C#で、float値を、ToString()して、float.Parseすると若干のずれがあります。
どの程度ずれるのかについて書きました。
当然、これはずれる場合とずれない場合があるのですが、今回はUnityのRandam値で検証しました。

最初に結論

結論から言うと、利用する値の範囲によって変わります。

-9999~9999ぐらいの値の範囲であれば、±0.0009765625ぐらいのずれでした。
-999~999ぐらいの値の範囲だと、6.103516E-05ぐらいですね。
これらの値は、アバウトなので参考程度に。

※コメント欄にToString("G9")すれば、改善するという指摘がありました。
ありがとうございます。
恐ろしいことに、全くずれなくなりました。ずれが0です。
ということで、下記の記事は普通に引数無しのToString()した場合の記事です。
ToString("G9")にしたら、こんな余計なことする必要ないのかもしれません。

どうやって調べたか

かなり適当なんですが、コードです。
float値の2つの値の範囲って、無限にあるのでランダムでやってます。
毎フレーム1000回試してみて、最大誤差が現れた時にDebug.Logしています。

StringToNumber.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StringToNumber : MonoBehaviour
{
    float _minDif = 0;
    float _maxDif = 0;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        for (int i = 0; i < 1000; ++i)
        {
            float f = Random.Range(-9999f, 9999f);
            string s = f.ToString();
            float v = float.Parse(s);
            float d = v - f;
            if (d < 0f)
            {
                if (d < _minDif)
                {
                    _minDif = d;
                    Debug.Log("MIN DIF:" + _minDif);
                }
            }
            else
            {
                if (d > _maxDif)
                {
                    _maxDif = d;
                    Debug.Log("MAX DIF:" + _maxDif);
                }
            }
        }
    }
}

誤差があると何が困るのか

実際に起きた問題として、オブジェクトの位置を保存する場合です。
このオブジェクトは、rangeMinからrangeMaxの間にしか置けないものでした。
範囲外だと問題がでている状態になって、灰色で表示されるようになります。
範囲ぎりぎりの地点は、位置をスナップできるようにしてあったとします。

仮に、rangeMaxの地点で、位置を文字列として保存しました。
で、ロードし直すと誤算によって問題がでている状態になっていることがあるわけです。

対策1

誤差を許容しましょう。
-9999~9999ぐらいの値の範囲であれば、±0.0009765625ぐらいのずれでした。
なので、0.001ぐらいのずれは許容するようなコードにする。
(私だったら怖いので、0.01ぐらいにしますが)

rangeCheck.cs
bool rangeCheck(float value)
{
    float eps = 0.001;
    if ((rangeMin-eps<value) && (value<rangeMax+eps)) return true;
    return false;
}

対策2

バイナリで保存する。

結論

すごくどうでも良い話だったかもしれませんが、文字列化して数値に戻すと少しだけ数値が変わるので、少しだけ許容幅を設けよう。
その際の許容範囲ってどのぐらいにしたら良いのかって話でした。

文字列で数値を保存するのって便利だけど、問題がでることもあるよねって話です。
(というか実際に問題が出たので、記事に残しておこうと思うのでした)

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

BlazorWebAssemblyとAzureFunctionsではてなブックマークの人気エントリーを表示するWebサービスを作ってみた

はじめに

本記事は小島優介さんが運営するコミュニティであるServerlessLT大会初心者向けという場で7月に発表した内容を記事に起こしたものです。(期間が空いてしまいました。。)
発表経験が少なかったですし、すごく初歩的な内容でしたが、聴講者の皆さんから「ナイスチャレンジ!」などポジティブフィードバックをたくさんもらえたのでとても敷居が低いと思います。毎月開催されるので興味のある方はぜひ参加してみてください。

使用した技術

フロントエンドにBlazorWebAssembly、サーバーサイドにAzureFunctionを使用しました。
BlazorWebAssemblyについてはこちらにまとめたので参照ください。
AzureFunctionについて簡単に説明すると、
- クラウド上でコード(関数)をサーバなしに手軽に実行できるサービス
- 利用者はサーバの管理を気にする必要はなく、アプリケーションロジックに集中できる
- ローカルで動作確認できる
といった感じでお手軽に利用することができます。

はてなブックマークの人気エントリーを表示させるためにやったこと

やったことはとても簡単です。

  • VisualStudioでAzureFunctionのプロジェクトを立ち上げるとテンプレートの関数があるので、その中身を書き換えて、はてなブログの人気エントリーを取得するAPIを自作

  • VisualStudioでBlazorWebAssemblyのプロジェクトを立ち上げるとテンプレートが表示されるのでそれを書き換えてRazorファイルから自作APIを呼び出し、はてなブログの記事一覧を表示

見た目などはとりあえず置いておいて、とにかく人気エントリーを表示させるということをしました。

人気エントリーの取得方法

はてなブログにはRSSというWebサイトの更新情報をXML形式で配信する仕組みがあるのでそのXMLを解析して記事のタイトルとURLのディクショナリを作成し、Json形式に変換して返すというAPIを自作しました。(RSSという技術は初めて知りましたが、昔はブログなどの更新を取得するためによく利用されていたようです。今はそういった需要が無くなって廃れつつあるようです。。)

下記のようなXML形式のデータを得られます。
2020-12-07_17h16_21.png
XMLの解析は以下のようにして行いました。

まずはchannelを取得し、channelの中にあるitemを全て取得します。
item一つ一つがブログの情報になっているので、欲しい情報であるtitleとlinkを抽出します。
コードは以下です。

        #region フィールド

        /// <summary>
        /// はてなブログのRSSを取得するためのURL
        /// </summary>
        private static string m_feedUrl = "https://blog.hatenablog.com/rss";

        #endregion

        #region 公開サービス

        /// <summary>
        /// はてなブログのRSSをXMLを解析してブログタイトルとURLのリストをJson形式で返す
        /// </summary>
        /// <param name="req"></param>
        /// <param name="log"></param>
        /// <returns></returns>
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // Rssからchannelを読み込む
            var element = XElement.Load(m_feedUrl);
            var channelElement = element.Element("channel");

            // itemを読み込む
            var itemElements = channelElement?.Elements("item");

            // itemからブログTitleとLinkを抽出して独自のJson形式に変換する
            var rssInfo = new RssInfo();
            foreach (var item in itemElements)
            {
                rssInfo.TitleList.Add(item.Element("title")?.Value);
                rssInfo.UrlList.Add(item.Element("link")?.Value);
            }
            var jsonData = JsonConvert.SerializeObject(rssInfo);
            return new OkObjectResult(jsonData);
        }

        #endregion
    /// <summary>
    /// RSSから取得した情報
    /// </summary>
    public class RssInfo
    {
        /// <summary>
        /// タイトルリスト
        /// </summary>
        [JsonProperty]
        public List<string> TitleList { get; set; } = new List<string>();

        /// <summary>
        /// URLリスト
        /// </summary>
        [JsonProperty]
        public List<string> UrlList { get; set; } = new List<string>();
    }

上記のような感じではてなブログの情報を取得するAPIを作ったら、Azureポータルにリソースを作って発行します。

発行ができたら、AzureポータルのCORS設定というものを行います。
APIを呼び出す側のドメインをここに追加することでアクセスを許可できます。
CORSについてはこちらの記事が分かりやすかったです。
2020-12-07_17h38_17.png
また、ローカルでデバッグしたい場合は、以下のようにAzureFunctionsのプロジェクト内に存在するlocal.settings.jsonに以下の設定を追加します。

"Host": {
    "LocalHttpPort": 7071,
    "CORS": "*",
    "CORSCredentials": false
  }

BlazorからAPIをたたいてブログの情報を表示する

記事にした要領でAPIをたたきます。
razorファイルの@code内でAPIをたたき、取得できたJson文字列をデシリアライズします。
OnInitializedAsyncをオーバライドしてAPIをたたきました。OnInitializedAsyncはページがレンダリングされるタイミングで実行されるみたいです。詳しくはこちら

@code {
    protected override async Task OnInitializedAsync()
    {
        var response = await Http.GetAsync(@"https://hatenaXXXXXXX");
        var jsonData=response.IsSuccessStatusCode ?
            await response.Content.ReadAsStringAsync() :
            "Failed";
        var rssInfo = JsonConvert.DeserializeObject<RssInfo>(jsonData);
    }

取得ができたら、razorファイル内の@page部分にHTML形式で出力結果を記述しました。
すると、以下のように表示されました!!
2020-12-07_18h14_11.png
ちなみに作成したページはこちらです。

詰まった点

開発中つまずいた点を共有します。超初心者なのでつまずき内容も初心者ですが、私と同じようにWeb、AzureFunction、Blazorについて全然知らないという方には少しは役に立つと思います。
- CORSというものを知らなかったので、作成したAPIがたたけない理由がわからなくてつまりました。
- CORS設定をしてもAPIがたたけないこともありました。Azureポータルを確認すると原因不明のエラーが出ていました。リソースを作成し直してもう一度発行すると解消できました。
- BlazorWebAssemblyを使うとVisualStudioが時々不安定な動作をしてしまうことがありました。再起動で解消できました。

さいごに

自己学習のためにすごく簡単ではありますが、Webサービスを作成してみました。私もまだまだ勉強中なのでわかり辛いところばかりだったとは思いますが最後までお読みいただきありがとうございます。BlazorやAzureなどこれからも勉強していきたいと思います。

Blazorの情報収集に良さそうなサイトを見つけたので共有しておきます。(英語です。)
Blazor University

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

変数スコープを誤解していた件

自己紹介

昔はDelphi、ここ数年はC#で業務アプリを開発しているarimooです。
もうLINQとリフレクションのない世界には戻れそうにありません。

Qiita初投稿、Advent Calendar初参加です。よろしくお願い致します。

本記事は「C# その2 Advent Calendar 2020」19日目です。

発端

いつも参考にさせて頂いている「未確認飛行C」の記事C# の null 判定の話を拝見していて(記事の趣旨とは無関係な部分で)気になるコード例が...

(引用開始)

void M(A a)
{
    if (a.X is not { } x) return; // null だったら early return。

    // x を使って何か処理をする。
    // ここでは x に非 null な値が入っているはず。
}

(引用終了)

あれっ!? if文カッコ内の"x"のスコープってifブロックで閉じてなかったっけ...?

検証

※環境はVisualStudio2019、ターゲットフレームワークを.NET 5.0にしたWinFormsアプリで検証しています。

という訳で適当なクラスAを作成し、記事中の"x を使って何か処理をする。"にDebug.WriteLineでxを表示させます。

クラスAとメソッドM
class A
{
    public string X;
}

void M(A a)
{
    if (a.X is not { } x) return; // null だったら early return。

    // x を使って何か処理をする。
    Debug.WriteLine($"x: {x}"); //ここでエラーが出ていない!
}

VS上でこのコードを書いてもエラーが検出されないので、この時点で"x"がifブロックの外で参照できる事、私が変数スコープを誤解していた件が確定っ!

早くもタイトルを回収してしまいましたが、でもまぁ折角コードを書いたので気を取り直して上記を呼び出してみます。

メソッドMの呼び出し側
//XがNULL
A a = new(); //Viva! C#9.0
M(a); //early returnが効いて、何も出力されない

//Xを埋める
a.X = "hoge";
M(a); //"x: hoge"が出力される

...と、想定通り(※)の挙動になりました。

※私のではなく、元記事の想定の通り...

なぜ誤解したか...?

ぱっと見の印象

for文
for (int i = 0; i < 10; i++)
{

}
Debug.WriteLine($"{i}"); //iはスコープ外、ダメ

string[] strs = new string[] { "aaa", "bbb", "ccc" };

foreach (var s in strs)
{

}
Debug.WriteLine($"{s}"); //sはスコープ外、ダメ

上記for/foreach文やメソッドの引数のように、{}ブロック外で使えないという印象が強く効いていました。

マイクロソフトのドキュメントの誤読

"expr is type varname"に対応した当時に読んだ内容と同じかどうかは不明ですが...

マイクロソフトのドキュメント中、

(引用開始)

C#
expr is type varname

expr が true であり is が if ステートメントに使用されている場合は、varname は if ステートメント内のみに割り当てられます。 varname のスコープは、is 式から if ステートメントを閉じるブロックの末尾までになります。 他の任意の場所に varname を使用すると、割り当てられていない変数の使用によるコンパイル時エラーが生成されます。

(引用終了)

...の赤字部分を見て早とちりして、その先の青字をさらっと流してしまっていました。
冷静に考えるとスコープ外なら"現在のコンテキストにvarnameは存在しません"になるはずですね...

※とはいえ"varname のスコープは、is 式から if ステートメントを閉じるブロックの末尾までになります。"はダウトかな~。

"割り当てられていない変数の使用によるコンパイル時エラー"という事は、割り当てればエラーにならないハズ! ...という事で、検証した内容をコメント文に記載しています。
(最初の検証で上手く行く事は判っていますが、実際に弄ってみるとフロー解析の賢さを体験できます!)

検証コード
string s1 = null;

if (s1 is string w1)
{
    //ここに入る場合、w1はs1の中身で埋まっている
    w1 += ":TRUE"; //埋まっているから+=もOK
}
else
{
    //ここに入る場合、w1は未割当の状態
    w1 = "FALSE"; //ここでw1への代入で、WriteLineでも使える状態になる
    //w1 += "FALSE"; //先にこれはダメ。w1は"未割当"エラーになる

    //return; //returnするならWriteLineまで行かないからw1埋めなくてもOK!
}

Debug.WriteLine($"{w1}"); //elseブロックをコメントアウトすると、w1は"未割当"エラーになる

w1をifブロック外で使うためにはelse側で埋めてあげるかreturnする必要がある、という事ですね!
(is notにするとifブロック側で埋めるかreturnする必要があります)

まとめ

便利に使っているif (obj is Hoge h)構文のhが、ifブロックの外側でも使えるとは目から鱗でした!

※今のところ、元記事にあるような演算子オーバーロード回避の需要が発生しない限り使わないとは思いますが...(^^;
(そもそも演算子オーバーロード自体を余り使っていない...)

初投稿&初Advent Calendarが自分のうっかり記事というのもどうかと思いましたが、同じように誤解している方もいるかもしれないと思い、書かせて頂きました。参考になれば幸いです!

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

C# staticを大雑把に理解する

使い方① ---using static ディレクトリ---

これを使うとコードが冗長にならず、読みやすくなる。
例を出すと、

System.Console.Write("");       // 改行なしで出力
System.Console.WriteLine("");   // 改行ありで出力
System.Console.Read();          // 1文字だけ読み込む
System.Console.ReadLine();      // 1行を読み込む

という4つのものがある場合、いつもなら

using System;   // 名前空間の使用

と付けて

Console.Write("");
Console.WriteLine("");
Console.Read();
Console.ReadLine();

こうして終わりだろう。
だが、ここを

using static System.Console;

と入力してあげることで

Write("");
WriteLine("");
Read();
ReadLine();

ここまで短縮することができる。
これは「System.Consoleという部分までは全て共通で使うから、メモリに事前に確保しておいてね」という意味あいになる。
プロパティに使うものとこれは全くの別物らしい。知らなかった……

Unity使用者向け

using UnityEngine;
Debug.Log("");
Mathf.Clamp();
Input.GetAxis();

これが

using static UnityEngine.Debug;
using static UnityEngine.Mathf;
using static UnityEngine.Input;
Log("");
Clamp();
GetAxis();

これになる。
基本的な部分は同じで、『 . 』で区切られている部分まではこの方法で省略することができる。
よく使用するものに関してはこれでいいが、たまにしか使わないものの場合ずっとメモリが確保され続けてしまうので、staticは使用しない方がいい。

使い方② ---通常のstatic---

・staticはひとつしかないものに使う。
・よく呼び出される場合に、事前にメモリを確保してあげる。

説明①

力尽きたのでまた今度……

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

uGUIのGridLayoutGroupがオートレイアウト対応してないので対応させるやつ書いた

概要

GridLayoutGroupのCellSizeを自動調整します。

使い方

GridLayoutGroupと同じオブジェクトに付与してください。
もしくは、付与した際に自動でGridLayoutGroupが連鎖的に付与されます。
なお、Constraintを何に設定するかで、調整項目が違うので注意。

  • Flexible
    • CellSizeが完全に自動調整されます。事前入力値は無視します。
  • FixedColumnCount
    • CellSizeのXを自動調整します。Yは事前入力値を継承。
  • FixedRowCount
    • CellSizeのYを自動調整します。Xは事前入力値を継承。
AutoGridLayoutOverrider.cs
using UnityEngine;
using UnityEngine.UI;

namespace CustomUIParts
{
    /// <summary>
    /// UnityEngine.UI.GridLayoutGroup をAutoLayout仕様にする
    /// </summary>
    [RequireComponent(typeof(GridLayoutGroup))]
    public class AutoGridLayoutOverrider : MonoBehaviour
    {
        /// <summary>
        /// オートレイアウト対応するGridLayoutGroup
        /// </summary>
        [SerializeField] GridLayoutGroup grid = default;

        /// <summary>
        /// サイズ調整に使用するRectTransform
        /// </summary>
        RectTransform rect = default;

        #region サイズ変動要素の比較用キャッシュ
        // Grid内オブジェクトの個数
        int beforeChildCount;
        // RectTransformからのデータ
        float beforeWidth, beforeHeight;
        // GridLayoutGroupからのデータ
        Vector2 beforeCellSize;
        Vector2 beforeSpacing;
        GridLayoutGroup.Axis beforeAxis;
        GridLayoutGroup.Constraint beforeConstraint;
        int beforeConstraingCount;
        #endregion


        /// <summary>
        /// InspectorでのReset動作定義
        /// </summary>
        void Reset()
        {
            grid = GetComponent<GridLayoutGroup>();
        }

        /// <summary>
        /// Unityライフサイクル関数
        /// </summary>
        void Awake()
        {
            // サイズ調整の基準となるRectTransformを取得
            rect = GetComponent<RectTransform>();
            while (rect.rect.width == 0 && rect.rect.height == 0)
            {
                // Streach前提でサイズが取得できないときは、親・祖先オブジェクトに遡って取得しにいく
                rect = rect.transform.parent.GetComponent<RectTransform>();
            }
        }

        /// <summary>
        /// Unityライフサイクル関数
        /// </summary>
        void Update()
        {
            ResetLayoutSize();
        }

        /// <summary>
        /// レイアウトサイズ変更
        /// </summary>
        void ResetLayoutSize()
        {
            // レイアウト状態に変更が無ければスキップ
            bool skipFlag =
                beforeWidth == rect.rect.width &&
                beforeHeight == rect.rect.height &&
                beforeChildCount == grid.transform.childCount &&
                beforeSpacing.x == grid.spacing.x &&
                beforeSpacing.y == grid.spacing.y &&
                beforeAxis == grid.startAxis &&
                beforeConstraint == grid.constraint;

            // 幅固定指定の場合は、前回サイズから変更がないか追加チェック
            switch (grid.constraint) {
                case GridLayoutGroup.Constraint.FixedColumnCount:
                    skipFlag &= beforeCellSize.y == grid.cellSize.y;
                    skipFlag &= beforeConstraingCount == grid.constraintCount;
                    break;
                case GridLayoutGroup.Constraint.FixedRowCount:
                    skipFlag &= beforeCellSize.x == grid.cellSize.x;
                    skipFlag &= beforeConstraingCount == grid.constraintCount;
                    break;
            }

            if (skipFlag)
            {
                return;
            }

            // 何かしら状況が変わったのでレイアウト設定を変える

            float x = grid.cellSize.x;
            float y = grid.cellSize.y;

            switch (grid.constraint)
            {
                // x,y:ともに可変(領域全体を埋める)
                case GridLayoutGroup.Constraint.Flexible:
                    int childCount = grid.transform.childCount;
                    int i = 0;
                    while (Mathf.Pow(++i, 2) < childCount) { }

                    int tateNum, yokoNum;
                    if (Mathf.Pow(i, 2) == childCount)
                    {
                        tateNum = yokoNum = i;
                    }
                    else if (grid.startAxis == GridLayoutGroup.Axis.Horizontal)
                    {
                        yokoNum = i;
                        tateNum = childCount / i + (childCount % i != 0 ? 1 : 0);
                    }
                    else
                    {
                        // 縦方向優先に並べる動作は挙動が特殊
                        yokoNum = i;
                        if(childCount <= i * (i-1))
                        {
                            tateNum = childCount / i + (childCount % i != 0 ? 1 : 0);
                        }
                        else
                        {
                            tateNum = i;
                        }
                    }

                    x = (rect.rect.width - grid.spacing.x * (tateNum - 1)) / tateNum;
                    y = (rect.rect.height - grid.spacing.y * (yokoNum - 1)) / yokoNum;
                    break;

                // width のみ可変
                case GridLayoutGroup.Constraint.FixedColumnCount:
                    x = (rect.rect.width - grid.spacing.x * (grid.constraintCount - 1)) / grid.constraintCount;
                    break;

                // height のみ可変
                case GridLayoutGroup.Constraint.FixedRowCount:
                    y = (rect.rect.height - grid.spacing.y * (grid.constraintCount - 1)) / grid.constraintCount;
                    break;
            }
            beforeCellSize = grid.cellSize = new Vector2(x, y);

            // 更新処理スキップ判定用に諸々キャッシュ
            beforeChildCount = grid.transform.childCount;

            beforeWidth = rect.rect.width;
            beforeHeight = rect.rect.height;

            beforeSpacing = grid.spacing;
            beforeAxis = grid.startAxis;
            beforeConstraint = grid.constraint;
            beforeConstraingCount = grid.constraintCount;
        }
    }
}

変更ログ

2020/12/16
最初はUpdateにプラットフォーム依存処理を入れていたが、
別プラットフォームでビルドしたら反映されなかったので修正。

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

One .NET時代のC# Web開発における技術選択を考える

はじめに

レガシーC# (.NET Framework) + ASP.NET MVCでWeb開発をしているIosif Takakuraと申します。この記事については私個人の見解であり、所属する組織の見解では無いことを申し上げておきます。

最近.NET 5.0がリリースされ、多くのプラットフォームでの実装が.NET 5.0から続くOne .NETに集約されつつあります。しかしながら、エンタープライズな.NETの世界では.NET Frameworkが生き残っていることも多いと聞きます。そのような世界では様々な事情から新しい.NETへの移行が考えられていないことも多いです。

その一方で、最近ではLinux上で.NETを動かす例があったり、ゲーム開発の世界で Unity1が幅広く用いられるようになり、.NETの世界への参入障壁が徐々に下がってきています。その中で、CySharpの河合宜文さんによる「C#大統一理論」という、サーバサイドとクライアントサイドを全てC#で統一する開発手法が提案されていたりとC#の世界も変化しつつあります。

また、BlazorなどのWebAssembly技術の登場でWeb開発の世界にも変化が予想される中、WebではJavaScriptやTypeScriptでサーバサイドも開発しようとする流れ、とりわけTypeScriptでサーバサイドとクライアントサイドの世界をひとまとめにしようというフレームワークもいろいろ登場し、「TypeScript大統一理論」もあり得る時代になってきました。

その中で今回はどうやって技術選択をすべきか考えていきたいと思います。

今後のC#を見据えた展望

先日、C# Tokyo オンライン フリートーク・ディスカッション 2020/12というオンライン座談会に参加してきたのですが、参加者はエンタープライズ系開発者が多く、その中で.NET Frameworkを使っているエンタープライズ開発者ならではの悩みも多く上がってきていました。その議論の結果を踏まえつつ、今後のC#の展望について私が思っていることをお話しします。

.NET Frameworkから.NETへの移行について考える

開発者の側からしてみれば、新しい.NETやC#に惹かれてしまうのもわからなくも無いですが、システム管理者サイドや経営サイドからしてみると.NET Frameworkを使い続けた方が楽という状況も見えてきました。ざっと考えると、以下のような理由が挙げられます。

  1. 新しい技術の学習コストが大きく、情報を得た上で検証する機会が少ない
  2. 「情シス」が忙しすぎて、検証している余裕がない
  3. 新しい環境へ移行するリスクを冒してまで新しい技術を使うメリットが見えてこない

この場合は、新しい技術に移行する価値をユーザーサイドや経営サイドに説明できないと新しい技術を使ってもらえるかは難しいと思います。とりわけ、ITを収益システムと考えていない会社の場合は厳しいといえるかもしれません。そういう.NET Frameworkでよいやという企業がある限り、Microsoftもサポートを切れないのでは無いかと思っています。

2020-12-15 追記

しかしながら、パフォーマンスを考えると.NETの方に分が上がります。TechEmpowerのベンチマークのComposite scoresの結果を見てみると一目瞭然です。新しい.NET Coreの方は6位に位置しているのに対し、.NET FrameworkのASP.NETは103位、ブービーです。ここまで来ていると性能差ははっきり見えてきます。それに加え、.NET Frameworkは今後新機能の追加は無いとのことなので、おそらく現状維持から衰退していく一方だと思われます。そうなると、新規開発では.NETで開発した方がよいように思われるのです。

C#とTypeScriptと大統一理論と

最近、フロントエンドをC#で書けるBlazorというUIフレームワークが出てきました。Blazorを使う場合でもJavaScriptを書かなくてよいというわけではありませんが、そうなってくるとフロントエンドのNode.jsエコシステムとの連携が問題になってくると思います。

なお、現状ではフロントエンドはJavaScript/TypeScriptの天下なわけですが、フロントエンドとバックエンドを一つの言語で、一つの型システムで書きたいという要望はあるわけです。その動きとしてTypeScript用のPrismaというORMを使用し、それらを組み込んだFrourioBlitz.jsといったフルスタックのTypeScriptフレームワークが登場しはじめています。そんなTypeScript界隈からの挑戦状としてRe: Rails を主戦場としている自分が今後学ぶべき技術についてという記事が話題に上がりましたが、Blazorの普及は以下の2つの課題が解決されないと厳しいのではないかと思っています。

  • 「WASM GCが仕様化されるまで、出力サイズが厳しい」2問題
  • C#ユーザーにしか使ってもらえないかもしれない問題

もちろんJavaScriptエコシステムの側にもNode.jsとDenoの問題があり、エコシステムの混乱は起こりうるでしょうが。

デスクトップかWebか

次に問題になるのがシステムをデスクトップアプリとして作るのか、Webアプリとして作るのかという点です。私は普段からC#をWeb開発に使っていますが、先日のフリートークではWPFなどのデスクトップアプリケーションを開発している人も多かったです。

その中で気になっていたのが、C#でWeb開発をすることの意味です。正直なところUI面ではWebはデスクトップアプリケーションに及ばないわけです。WebでリッチなUIを求めるならNode.jsエコシステムの助けを借りる必要が出てきますが、Webで開発を行う以上、バックエンドのC#の世界とフロントエンドのNode.jsの世界は分断された状態になります。デスクトップアプリケーションなら全てC#の世界で完結できるわけです。ゲームなどの最新技術が求められる分野ではC#大統一理論の恩恵を受けることもできます。おそらく、C#でWebを作る理由は配布や管理の手間の軽減にあると思われますが、現時点では配布や管理の手間はIntuneなどで格段に楽になったといえるでしょう。Xamarinで書く場合もAppleやGoogleのストア経由で配布できるので、昔みたいな配布の手間は少なくなったといえます。

そうなると、あえてWebで開発する意味はあるのでしょうか?そこには、開発者のレベルの問題があると思います。Webで開発するならスキルセットはWebの延長線上ですが、デスクトップアプリケーションやXamarinの場合はWebとは異なるスキルセットが必要だと思います。そういったネイティブアプリの育成に人が追いついていない現状もフリートークから感じられました。

それを考えると、デスクトップアプリケーションを作れるならデスクトップアプリケーションで作った方がよいものの、現実的には人が足りないので難しいと思います。

業務アプリケーション開発者から見たUnityの世界

また、私は、現時点でUnityの発展がC#を牽引するだろうと思っています。ゲームは業務システムとは正反対な、以下のような特徴があります。

  • それ自体が収益源となる
  • ある程度パフォーマンスやUIがよくなければ売れない
  • 最新技術の恩恵を受けやすい

そうなっていくと、C#のパイが広がるためにはゲーム開発がもっと盛んになっていくことだと思っています。.NETがLinuxで動くようになったことで、参入障壁はかなり下がったと思います。後はUnreal Engineを巡るAppleとEpic Gamesの問題でどれだけの人がUnityを使うかを注目する必要がありそうです。

まとめ

ということで、いいたいことをざっとまとめると以下のような感じになります。

  • Blazorは今後に期待するしかない
  • TypeScript大統一理論はC#のよきライバルとなると思われる
  • デスクトップアプリケーションを作れるならデスクトップアプリケーションを作るべき
  • C#エコシステムの発展はUnityの発展に期待

さて、現時点で堅実なWeb開発の組み合わせとしては、フロントエンドはNode.jsエコシステムに頼りつつ、バックエンドをASP.NET Core Web APIで書くことだと思います。それにしても、私がこんな環境を使えるようになるのはいつになるのでしょうか。


  1. ゲームエンジンの方。UbuntuのUIでもMicrosoft製のDIコンテナでもない。 

  2. mizchiさんの受け売り。 

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

テスタブルなコードを書くためのインターフェース入門

この記事は C# Advent Calendar 2020 17日目の記事です。

はじめに

C#を書いている人なら一度はインターフェースという機能を見たことがあるだろう。
メソッド、プロパティなどの定義だけを記述しておき、実装を強制できるあれのことだ。

一体あんなものが何の役に立つんだろうか。
私は最初そんなことを思っていた。

しかし、テストコードを書きたいと思ったときインターフェースはとても役に立つことに気づいたんだ。
この記事ではテストが難しいコードに対し、インターフェースを使用し、テスタブルなコードに変更していくことで、インターフェースについて理解を深められたらと思う。

対象者

この記事は以下のような人を対象とする。

  • インターフェースの使い所やメリットがいまいち良くわからないという人
  • テスタブルなコードを書きたいと思っている人

それでは始めよう。

例題

入力した文字列をラベルプリンタに印刷するシステムを想定しよう。
システムの仕様は以下の通りだ。

  • ラベルプリンタにはWebAPIが備わっているため、そのAPIを経由してラベルを印刷する。
  • 印刷が完了したらユーザに知らせる。
  • 何かしらの理由により印刷できなかった場合は印刷に失敗したことをユーザに知らせる。

これらの仕様を満たすような以下のようなコードがあったとしよう。
このときPrintServiceクラスに対してテストコードを書けるだろうか?
(コードに不備があるかもしれないが、あくまで例題用のサンプルコードとして見ていただければと思う。)

Main

Program.cs
using System;

namespace QiitaAdventCalendar2020
{
    class Program
    {
        static void Main(string[] args)
        {
            var str = args[0];
            var svc = new PrintService();
            if(svc.Print(str, out string errMsg))
            {
                Console.WriteLine($"印刷が完了しました。[{str}]");
            }
            else
            {
                Console.WriteLine(errMsg);
            }
        }
    }
}

PrintServiceクラス

PrintService.cs
using System;
namespace QiitaAdventCalendar2020
{
    public class PrintService
    {
        private readonly LabelPrinter _printer;

        public PrintService()
        {
            _printer = new LabelPrinter(new Uri("http://webprinter/api"));
        }

        public bool Print(string contents, out string errMsg)
        {
            if(!_printer.IsRunning())
            {
                errMsg = "プリンタの電源が入っていません。";
                return false;
            }

            if(!_printer.Print(contents))
            {
                errMsg = "印刷に失敗しました。";
                return false;
            }

            errMsg = "";
            return true;
        }
    }
}

LabelPrinterクラス

LabelPrinter.cs
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;

namespace QiitaAdventCalendar2020
{
    public class LabelPrinter
    {
        private static readonly HttpClient __client = new HttpClient();
        private readonly Uri _uri;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="uri">ラベルプリンタのURI</param>
        public LabelPrinter(Uri uri)
        {
            _uri = uri;
        }

        /// <summary>
        /// プリンタが起動しているかどうかを確認する
        /// </summary>
        /// <returns>true: 起動している、false: 起動していない</returns>
        public bool IsRunning()
        {
            var uri = new Uri(_uri, "status");
            var contents = __client.GetStringAsync(uri).Result;
            var status = JsonConvert.DeserializeObject<GetResultForStatus>(contents);
            return status.Running;
        }

        /// <summary>
        /// 印刷する
        /// </summary>
        /// <param name="contents">印刷する内容</param>
        /// <returns>true: 印刷成功、false: 印刷失敗</returns>
        public bool Print(string contents)
        {
            // 印刷用のパラメータを作成する
            var postData = new PostDataForPrint()
            {
                Text = contents
            };
            var json = JsonConvert.SerializeObject(postData);

            // 印刷する
            var uri = new Uri(_uri, "print");
            var httpContent = new StringContent(json, Encoding.UTF8, @"application/json");
            var response = __client.PostAsync(uri, httpContent).Result;

            return response.StatusCode == HttpStatusCode.OK;
        }

        /// <summary>
        /// GET /status の結果
        /// </summary>
        private class GetResultForStatus
        {
            [JsonProperty("running")]
            public bool Running { get; set; }
        }

        /// <summary>
        /// POST /print のデータ
        /// </summary>
        private class PostDataForPrint
        {
            [JsonProperty("text")]
            public string Text { get; set; }
        }
    }
}

テストが難しいコードとは

今のままPrintServiceクラスに対してテストコードを書こうとするとどうなるだろうか。

まずラベルプリンタを準備して・・・。
テストに応じて電源を付けたり、消したり・・・。

そんなことできるはずがない。

テストコードを動かすために一人一台ラベルプリンタを準備するのは難しい。
仮に準備できたとしてもテストごとにラベルプリンタの電源を付けたり、消したりするなんてできないだろう。

このままではPrintServiceクラスはテストコードを書くことができないのだ。

このように外部のデバイスへを利用したり、データベースに接続するなど、外部のものに依存しているとテストコードを書くのは途端に難しくなる

テスタブルなコードに改善する

どうすればテスタブルなコードにできるだろうか。
ここからは実際にテスタブルなコードに改善していこう。

インターフェースに依存する

今のコードの依存関係を改めて整理してみよう。
スクリーンショット 2020-12-13 23.30.32.png
この依存関係を見て分かる通り、PrintServiceクラスはLabelPrinterクラスを直接使用しているため、テストしようとするとLabelPrinterクラスの実装にもろに影響を受けてしまう。

LabelPrinterクラスの実装を気にせずに使用する方法は無いだろうか?
こういうときこそインターフェースの出番だ。

LabelPrinterクラスに依存するのではなく、LabelPrinterクラスのインターフェースに依存するように変更してみよう。
(このことを依存関係を逆転させると言ったりもする。以下の図を見ると矢印の方向が逆転していることが分かる。)
スクリーンショット 2020-12-13 23.32.03.png
LabelPrinterクラスのメソッドをすべてインターフェースとして抽出する。

ILabelPrinter.cs
namespace QiitaAdventCalendar2020
{
    public interface ILabelPrinter
    {
        bool IsRunning();
        bool Print(string contents);
    }
}

そして、LabelPrinterクラスはこのインターフェースを実装するようにする。

namespace QiitaAdventCalendar2020
{
    // インターフェースを実装するように書き換える
    //public class LabelPrinter
    public class LabelPrinter : ILabelPrinter
    {
        // 以下全く同じのため省略
    }
}

あとは、PrintServiceクラスをインターフェースに依存させるようにするだけだ。

using System;
namespace QiitaAdventCalendar2020
{
    public class PrintService
    {
        // インターフェースに依存するように変更する
        //private readonly LabelPrinter _printer;
        private readonly ILabelPrinter _printer;

        public PrintService()
        {
            _printer = new LabelPrinter(new Uri("http://webprinter/api"));
        }

        // 以下全く同じのため省略
    }
}

これを見て「結局ラベルプリンタを使わないといけないじゃないか」と思うかもしれない。
それは正しいが、その解決は次のステップに置いておこう。
まずはこのインターフェースに依存するということが何よりも重要だ。

インターフェースに依存したことにより、PrintServiceクラスはLabelPrinterクラスの実装に依存しなくなった。
さらに嬉しいことに、今まではたった一つのクラスしか使うことができなかったが、インターフェースを実装するクラスであれば何でも利用できるようになった。
具体的なクラスに依存するのではなく、インターフェースに依存することで、柔軟性が飛躍的に向上したしたことが分かると思う。

依存性を注入する(DI)

さて、先程解決できなかった次のステップに移ろう。
今のままではPrintServiceクラスの中でLabelPrinterを生成(new)しているため、どうしてもLabelPrinterの利用を避けられない。
それなら、内部で生成するのではなく外部で生成し、それを渡してあげるのはどうだろうか。
先ほど抽出したインターフェースを実装したクラスを受け取れるようにコードを変更してみよう。

using System;
namespace QiitaAdventCalendar2020
{
    public class PrintService
    {
        private readonly ILabelPrinter _printer;

        public PrintService()
        {
            _printer = new LabelPrinter(new Uri("http://webprinter/api"));
        }

        // ILabelPrinterを実装したクラスを受け取れるように
        // コンストラクタを追加した
        public PrintService(ILabelPrinter printer)
        {
            _printer = printer;
        }

        // 以下全く同じのため省略
    }
}

このように依存するものを外部から渡してあげることを依存性の注入(DI)と言ったりする。
これでPrintServiceクラスはILabelPrinterインターフェースを実装しているクラスであれば何でも受け取れるようになった。
これでテスト用のLabelPrinterクラスを設定するといったことも可能だ。

テストコードが書ける準備は整った。
早速テストコードを書いてみよう!

テストコードを書く

テストコードではILabelPrinterインターフェースを実装した、LabelPrinterの結果を偽装したクラスを作成し、先程準備したコンストラクタで偽装したクラスを渡してあげるようにする。

PrintServiceTest.cs
using System;
using Xunit;
using QiitaAdventCalendar2020;

namespace QiitaAdventCalendar2020.Tests
{
    public class PrintServiceTest
    {
        [Fact]
        public void Test_正常に印刷できた場合はエラーメッセージが空文字になること()
        {
            var printer = new FakeLabelPrinter()
            {
                IsRunningResult = true,
                PrintResult = true,
            };
            var svc = new PrintService(printer);
            var result = svc.Print("テスト", out string errMsg);
            Assert.True(result);
            Assert.Equal("", errMsg);
        }

        [Fact]
        public void Test_電源が入っていない場合はエラーメッセージが設定されること()
        {
            var printer = new FakeLabelPrinter()
            {
                IsRunningResult = false,
                PrintResult = false,
            };
            var svc = new PrintService(printer);
            var result = svc.Print("テスト", out string errMsg);
            Assert.False(result);
            Assert.Equal("プリンタの電源が入っていません。", errMsg);
        }

        [Fact]
        public void Test_印刷に失敗した場合はエラーメッセージが設定されること()
        {
            var printer = new FakeLabelPrinter()
            {
                IsRunningResult = true,
                PrintResult = false,
            };
            var svc = new PrintService(printer);
            var result = svc.Print("テスト", out string errMsg);
            Assert.False(result);
            Assert.Equal("印刷に失敗しました。", errMsg);
        }

        /// <summary>
        /// テスト用にLabelPrinterを偽装したクラス
        /// </summary>
        private class FakeLabelPrinter : ILabelPrinter
        {
            public bool IsRunningResult { get; set; }
            public bool PrintResult { get; set; }

            public bool IsRunning()
            {
                return IsRunningResult;
            }

            public bool Print(string contents)
            {
                return PrintResult;
            }
        }
    }
}

テストに応じてパラメータを自由に変更することで、ラベルプリンタを実際に準備したり、電源を入れたり、消したりしなくても、テストができるようになった。

まとめ

この記事ではテストが難しいコードに対し、テスタブルなコードに変更して行くことでインターフェースの使い所やメリットを紹介した。
この記事を読んでインターフェースについての理解やテスタブルなコードについての理解が少しでも深まったなら幸いだ。

それではまた。

TomoProg

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

C# : 配列・リストの中身を全て出力する方法

対象者

・指定なし

ソースコード

名前空間

using System;
using static System.Console;
using System.Collections.Generic;

配列とリスト

var array = new int[3] {1, 2, 3};
var list  = new List<int>() {4, 5, 6};

出力したい内容

1
2
3
4
5
6

for文で回した場合

for (var i = 0; i < array.Length; i++) WriteLine(array[i]);
for (var i = 0; i < list.Count; i++)   WriteLine(list[i]);

foreachで回した場合

foreach(var i in array) WriteLine(i);
foreach(var i in list)  WriteLine(i);

ForEachで回した場合

Array.ForEach(array, x => WriteLine(x));
list.ForEach(x => WriteLine(x));

結論

中身を全て出力したいなら、コードゴルフでもやっていない限りは可読性を考えforeach文を使った方がいい。

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

[C#]今更使うことはないかもしれないWindowsフック

Windowsのグローバルなメッセージフックで少しはまったのでメモします(グローバルな要素とは)。
マサカリお願いします。

ヘルパークラス(?)

InputHook.cs
public class InputHook : IDisposable
{
    private event HookProc KeyboardProcProperty;
    private event HookProc MouseProcProperty;

    private readonly IntPtr KeyboardHandle;
    private readonly IntPtr MouseHandle;

    public InputHook()
    {
        IntPtr hInstance = Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]);
        KeyboardHandle = SetWindowsHookEx(13, KeyboardProcProperty = KeyboardProc, hInstance, IntPtr.Zero);
        MouseHandle = SetWindowsHookEx(14, MouseProcProperty = MouseProc, hInstance, IntPtr.Zero);
    }

    public void Dispose()
    {
        UnhookWindowsHookEx(KeyboardHandle);
        UnhookWindowsHookEx(MouseHandle);
    }

    public delegate void MouseEvent(int x, int y);
    public event MouseEvent MouseMove;
    public event MouseEvent MouseLeftDown;
    public event MouseEvent MouseLeftUp;
    public event MouseEvent MouseLeftDoubleClick;
    public event MouseEvent MouseRightDown;
    public event MouseEvent MouseRightUp;
    public event MouseEvent MouseRightDoubleClick;
    public event MouseEvent MouseMiddleDown;
    public event MouseEvent MouseMiddleUp;
    public event MouseEvent MouseMiddleDoubleClick;

    public delegate void MouseWheelEvent(int x, int y, int delta);
    public event MouseWheelEvent MouseWheel;

    private IntPtr MouseProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode < 0)
        {
            return CallNextHookEx(KeyboardHandle, nCode, wParam, lParam);
        }

        MSLLHOOKSTRUCT data = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));

        int x = data.pt.x;
        int y = data.pt.y;

        switch (wParam.ToInt32())
        {
            case 0x0200:
                MouseMove?.Invoke(x, y);
                break;
            case 0x0201:
                MouseLeftDown?.Invoke(x, y);
                break;
            case 0x0202:
                MouseLeftUp?.Invoke(x, y);
                break;
            case 0x0203:
                MouseLeftDoubleClick?.Invoke(x, y);
                break;
            case 0x0204:
                MouseRightDown?.Invoke(x, y);
                break;
            case 0x0205:
                MouseRightUp?.Invoke(x, y);
                break;
            case 0x0206:
                MouseRightDoubleClick?.Invoke(x, y);
                break;
            case 0x0207:
                MouseMiddleDown?.Invoke(x, y);
                break;
            case 0x0208:
                MouseMiddleUp?.Invoke(x, y);
                break;
            case 0x0209:
                MouseMiddleDoubleClick?.Invoke(x, y);
                break;
            case 0x020A:
                MouseWheel?.Invoke(x, y, (short)((data.mouseData >> 16) & 0xFFFF));
                break;
        }

        return CallNextHookEx(KeyboardHandle, nCode, wParam, lParam);
    }

    public delegate void KeyEvent(Key key);
    public event KeyEvent KeyDown;
    public event KeyEvent KeyUp;

    private IntPtr KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode < 0)
        {
            return CallNextHookEx(KeyboardHandle, nCode, wParam, lParam);
        }

        KBDLLHOOKSTRUCT data = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));

        Key key = KeyInterop.KeyFromVirtualKey(data.vkCode);

        switch (wParam.ToInt32())
        {
            case 0x0100:
            case 0x0104:
                KeyDown?.Invoke(key);
                break;
            case 0x0101:
            case 0x0105:
                KeyUp?.Invoke(key);
                break;
        }

        return CallNextHookEx(MouseHandle, nCode, wParam, lParam);
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MSLLHOOKSTRUCT
    {
        public POINT pt;
        public uint mouseData;
        public uint flags;
        public uint time;
        public IntPtr dwExtraInfo;
    }

    [Flags]
    private enum KBDLLHOOKSTRUCTFlags : uint
    {
        LLKHF_EXTENDED = 0x01,
        LLKHF_LOWER_IL_INJECTED = 0x02,
        LLKHF_INJECTED = 0x10,
        LLKHF_ALTDOWN = 0x20,
        LLKHF_UP = 0x80,
    }

    [StructLayout(LayoutKind.Sequential)]
    private class KBDLLHOOKSTRUCT
    {
        public int vkCode;
        public uint scanCode;
        public KBDLLHOOKSTRUCTFlags flags;
        public uint time;
        public UIntPtr dwExtraInfo;
    }

    private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, IntPtr dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
}

使い方

MainWindow.xaml.cs
private InputHook InputHook { get; }

public MainWindow()
{
    InitializeComponent();

    InputHook = new InputHook();
    InputHook.KeyDown += key => Debug.WriteLine($"down {key}");
    InputHook.KeyUp += key => Debug.WriteLine($"up {key}");
}

private void Window_Closed(object sender, EventArgs e)
{
    InputHook.Dispose();
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む