20200323のC#に関する記事は11件です。

Roslynと複数形の不規則変化

C#の命名規則

C#ではMicrosoftの指針(識別子名 | Microsoft Docs大文字の仕様規則)に従って名前を付けることが多い。

今回の話題に関わる点だけ挙げると

  • 型名はPascalCase
  • 変数名はcamelCase

という規則は一般に使われていると思う。

変数名サジェスト

C#で開発しているとVisualStudioでもVSCode&OmniSharpでもRoslynによる優秀なインテリセンスが使用できる。
このインテリセンスには型名から変数名を提案する機能があり、変数名としてその変数の型名をPascalCaseからcamelCaseに変換したもの、あるいは型名をPascalCaseに従って分離したうえで一部を取り出してcamelCaseにしたものを提案してくれる。
具体的には以下のような提案が現れる。

//型名はPascalCase
class SpecialRapidServiceTrain { }

//この型の変数名として

//型名のPascalCaseをcamelCaseに変換したもの
private SpecialRapidServiceTrain specialRapidServiceTrain;

//型名から一部を取り出してcamelCaseにしたもの
private SpecialRapidServiceTrain rapidServiceTrain;
private SpecialRapidServiceTrain serviceTrain;
private SpecialRapidServiceTrain train;
private SpecialRapidServiceTrain special;
private SpecialRapidServiceTrain specialRapid;
private SpecialRapidServiceTrain specialRapidService;
//が提案される

またこの機能はある型のコレクションに対しては型名の複数名のcamelCase、あるいは変数名と同様に型名の一部を複数形にしたcamelCaseを提案する。

//型名を複数形のcamelCaseに変換したもの
private SpecialRapidServiceTrain[] specialRapidServiceTrains;

//型名を分離した上で一部を取り出して複数形のcamelCaseにしたもの
private SpecialRapidServiceTrain[] rapidServiceTrains;
private SpecialRapidServiceTrain[] serviceTrains;
private SpecialRapidServiceTrain[] trains;
private SpecialRapidServiceTrain[] specials;
private SpecialRapidServiceTrain[] specialRapids;
private SpecialRapidServiceTrain[] specialRapidServices;
//が提案される。

本題 : 不規則変化の名詞

ここからが本題。実はこの機能は不規則変化する名詞の複数形にも対応している。
分かりやすいところで言うと次のようなものがある。

//型SmallChildのコレクションに対して……
private SmallChild[] smallChildren;

//型LegalPersonのコレクションに対して……
private LegalPerson[] legalPeople;

じゃあこれはいったいどのくらいの不規則変化名詞に対応しているのか?
以下、不規則変化する名詞一単語からなる型名のコレクションに対してどのような変数名がサジェストされたかをまとめる。

英単語のリストは#3586. 外来複数形[plural][latin][greek][french][italian]をもとにした。なお、このリストは外来複数形のものなので英語の古い形に由来する不規則変化は載っていない。

正しく不規則変化されたもの

単数形 複数形 意味 サジェスト
bacillus bacilli バチルス菌 bacilli
stimulus stimuli 刺激 stimuli
alga algae algae
alumna alumnae 女子卒業生 alumnae
larva larvae 幼虫 larvae
addendum appenda 付録 appenda
bacterium bacteria バクテリア bacteria
datum data データ data
erratum errata 誤植 errata
codex codices 古写本 codices
analysis analyses 分析 analyses
axis axes axes
basis bases 理論的基礎 bases
oasis oases オアシス oases
parenthesis parentheses 丸かっこ parentheses
synopsis synopses 概要 synopses
synthesis syntheses 総合,合成 syntheses
criterion criteria 判断の基準 criteria
phenomenon phenomena 現象 phenomena
corps corps(単複同形) corps
rendezvous rendezvous(単複同形) 約束による会合 rendezvous

使用頻度の低そうな語にも対応していることに驚かされる。

規則変化と不規則変化両方がある単語

不規則変化が採用されたもの

単数形 複数形 意味 サジェスト
cactus cacti, cactuses サボテン cacti
radius radii, radiuses 半径 radii
syllabus syllabi, syllabuses 教授細目 syllabi
vertebra vertebrae, vertebras 脊椎 vertebrae
aquarium aquaria, aquariums 水族館 aquaria
curriculum curricula, curriculums 教育課程 curricula
memorandum memoranda, memorandums メモ memoranda
symposium symposia, symposiums シンポジウム symposia
apex apices, apexes 頂点 apices
index indices, indexes 指標 indices
matrix matrices, matrixes 行列 matrices

サボテンが登場するコードでも安心だ。

規則変化が採用されたもの

単数形 複数形 意味 サジェスト
antenna antennae, antennas antennae (触覚), antennas (アンテナ) antennas
formula formulae, formulas 公式 formulas
nebula nebulae, nebulas 星雲 nebulas
spectrum spectra, spectrums スペクトル spectrums
automaton automata, automatons 自動装置,ロボット automatons
ganglion ganglia, ganglions 神経節 ganglions
bureau bureaux, bureaus 事務局 bureaus
plateau plateaux, plateaus 高原 plateaus
tableau tableaux, tableaus tableaus
libretto libretti, librettos 脚本 librettos
tempo tempi, tempos テンポ tempos
virtuoso virtuosi, virtuosos 巨匠 virtuosos

フランス語・イタリア語由来の語はどれも規則変化が採用されているようだが理由があるのだろうか?

誤って活用されたもの

単数形のままcamelCaseにされたもの

単数形 複数形 意味 サジェスト
esophagus esophagi 食堂 esophagus
cirrus cirri, cirruses 巻雲 cirrus
genius genii,geniuses genii (守護神),geniuses (天才) genius
nimbus nimbi, nimbuses 乱雲 nimbus
corpus corpora, corpuses 集大成,言語資料 corpus
genus genera, genuses genus

corpusやgenusなどは使いどころも多そう(少なくともalumnaよりはコード中に出てくるだろう)なのに誤った形が提案されている。
どの語も末尾がsで終わっていることから考えるとRoslynが単数形を規則変化の複数形と誤認している疑いがある。そうだとすると「正しく不規則変化されたもの」に挙げたcorpsやrendezvousも単複同形だったためたまたま正しい結果が出ただけで、Roslyn側は謝って複数形の名詞と解釈しているのかもしれない。実際、型名が複数形だと

//型名Booksは複数形だと判断しているらしい
private Books[] books;

//型名Childrenは複数形ではないと判断している?
private Children[] childrens;

のような提案がなされるためだ。

存在しない複数形が提案されたもの

単数形 複数形 意味 サジェスト
chassis chassis(単複同形) 自動車の車台,シャシー chasses

これは不思議なパターンだ。本来存在しないはずの不規則変化の複数形*chassesがサジェストされている。(Weblioではハイパー英語辞書がこの形をchassisの複数形としているが、他の辞書による裏付けがないので疑わしい。もしOEDをお持ちの方がいたらこの形が載っているか調べていただきたい。自分でやればいいのだが図書館が閉館中なので調べにいけない……)

実装

RoslynのDeclarationNameCompletionProvider.csがこれらの処理に対応する実装のはず。
ただ肝心の名詞リスト(おそらく単数形から複数形を引ける辞書のようなものがあるはず)を見つけられていない。

追記
EnglishPluralizationService.cs - ReferenceSourceに処理を発見した。これだろうか?予想通り辞書の形となっている。

まとめ

Roslynの変数名サジェスト機能は不規則変化する名詞にそれなりに対応しているが、漏れもあるので過信してはいけない。
規則変化と不規則変化どちらの複数形もある名詞に対しては一方の形にしか対応できていないと思われるが、どちらの形の複数形を採用するかの方針はよくわからない。

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

Unityで簡単なゲームを作ってみた

作成したゲーム

  • 上から降ってくる岩を打ち落として、得点を稼ぐシューティングゲーム。岩が画面外に出てしまったらゲームオーバー。

  • Unityのバージョンは2018.4.19f1

  • プレイヤー移動はキーボード入力

  • 弾が岩に当たったら爆発のエフェクト

  • 弾を発射した際、Trail Rendererを使って弾の軌跡を出す

  • UIで得点を表示し、岩が画面外に行ったときにゲームオーバーを表示を出す

用意したもの

  • プレイヤー、障害物(岩とか)、弾、弾の軌跡、背景

使用した素材

ソースコード

プレイヤー(ロケット)を動かすスクリプト

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

public class RocketController : MonoBehaviour
{
    public GameObject bulletPrefab;

    void Update()
    {
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.Translate(-0.1f, 0, 0);
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.Translate(0.1f, 0, 0);
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(bulletPrefab, transform.position, Quaternion.identity);
        }
    }
}

Instantiate関数

  Instantiate(bulletPrefab,transform.position,Quaternion.identify);
//Instantiate(第1関数,第2関数,第3関数);
  • この関数ではPrefabからインスタンスを作り、任意の位置に生成する関数です
  • 第1関数にPrefab、第2関数にインスタンスを作成したい位置、第3関数にはインスタンスの回転角を指定します
  • 今回は弾のPrefab(bulletPrefab)を複製して発射したいのでこれを第1関数に、これをプレイヤーの位置に生成したい ので第2関数はtransform.position、第3関数の回転はQuaternion.identifyにすることで無回転になります

岩の制御

  • 岩を落とすスクリプト
RockController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RockController : MonoBehaviour
{
    float fallSpeed;
    float rotSpeed;

    void Start()
    {
        this.fallSpeed = 0.01f + 0.1f * Random.value;
        this.rotSpeed = 5f + 3f * Random.value;
    }


    void Update()
    {
        transform.Translate(0, -fallSpeed, 0, Space.World);
        transform.Rotate(0, 0, rotSpeed);

        if (transform.position.y < -5.5f)
        {
            GameObject.Find("Canvas").GetComponent<UIController>().GameOver();
            Destroy(gameObject);
        }
    }
}

Translate関数

  transform.Translate(0, -fallSpeed, 0, Space.World);
//transform.Translate(x, y, z, relative to);
  • 最初の3つの値はxyzどの軸に沿って移動するかという意味で、最後のrelative toにはSpace.WorldやSpace.Selfなどが入ります。Space.Worldにした場合、ワールド座標になります。ワールド座標とは原点から見た座標のことです。
  • 今回の場合、fallSpeedに従って落ちてきてほしいのでy軸のみ値が入っています

  • ランダムに岩を生成するスクリプト
RockGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RockGenerator : MonoBehaviour
{
    public GameObject rockPrefab;

    void Start()
    {
        InvokeRepeating("GenRock", 1, 1);
    }

    void GenRock()
    {
        Instantiate(rockPrefab, new Vector3(-2.5f + 5 * Random.value, 6, 0), Quaternion.identity);
    }
}

InvokeRepeating関数

InvokeRepeating("GenRock", 1, 1);
//InvokeRepeating(第1関数, n, m);
  • この関数はとても便利な関数です。第1関数に指定したものを、n秒後にm回呼び出す関数です。
  • 今回はGenRockという関数を1秒に1回呼び出したいのでこのようになります

背景を動かすスクリプト

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

public class BackgroundController : MonoBehaviour
{   
    void Update()
    {
        transform.Translate(0, -0.03f, 0);
        if (transform.position.y < -4.9f)
        {
            transform.position = new Vector3(0, 4.9f, 0);
        }
    }
}
  • 背景の大きさは決まっているのである一定のところまで移動したら、戻るようにif文で設定しています

発射する弾の制御

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

public class BulletController : MonoBehaviour
{
    public GameObject explosionPrefab;      //爆発エフェクトのPrefab

    void Update()
    {
        transform.Translate(0, 0.2f, 0);

        if (transform.position.y > 5)
        {
            Destroy(gameObject);
        }
    }

    void OnTriggerEnter2D(Collider2D coll)
    {
        //衝突したときにスコアを更新する
        GameObject.Find("Canvas").GetComponent<UIController>().AddScore();

        //爆発エフェクトを生成
        GameObject effect = Instantiate(explosionPrefab, transform.position, Quaternion.identity) as GameObject;
        Destroy(effect, 1.0f);

        Destroy(coll.gameObject);
        Destroy(gameObject);
    }
}

ややこしい部分の解説

//爆発エフェクトを生成
 GameObject effect = Instantiate(explosionPrefab, transform.position, Quaternion.identity) as GameObject;
 Destroy(effect, 1.0f);
  • Instantiate関数でGameObjectとしてキャストすることで、爆発エフェクトを加える
  • その後のDestroy関数でそのオブジェクトを消す。第2引数はオブジェクトを破壊するまでの時間(秒)です

UIの制御

UIController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIController : MonoBehaviour
{
    int score = 0;
    GameObject scoreText;
    GameObject gameOverText;
    public void GameOver()
    {
        this.gameOverText.GetComponent<Text>().text = "GameOver";
    }
    public void AddScore()
    {
        this.score += 10;
    }

    void Start()
    {
        this.scoreText = GameObject.Find("Score");
        this.gameOverText = GameObject.Find("GameOver");
    }

    void Update()
    {
        scoreText.GetComponent<Text>().text = "Score:" + score.ToString("D4");
    }
}

スコアの更新

scoreText.GetComponent<Text>().text = "Score:" + score.ToString("D4");
  • UIのテキスト部分に「Score:0000」と表示するためにscoreText.GetComponent.textで情報を入手する
  • score.ToString("D4")は整数を4桁表示するという意味です

作成したゲーム

簡単なゲーム制作2D.gif

問題点

  • 同じ場所で弾を連射すると、弾同士がぶつかってしまいそこに爆発エフェクトがでてしまう。

参考させていただいたサイト

おもちゃラボ 【Unity入門】60分でつくるシューティングゲーム

追記

  • GitHubにコード上げました!

GitHub

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

C# を触ってみたいけど自分の環境に .NET の SDK 入れるの何か嫌だという人向けハローワールド

以下の記事が思ったよりバズっててびっくりしたので気をよくして、これをきっかけに C# ちょっと気になるなという人向けの最初の、本当に最初に触ってみるための一歩を書いてみようと思います。

C# で出来ること一覧

コンソールアプリをブラウザーでお試し

この記事を書いた後に岩永さんから教えてもらいました。(知らなかったです)

Try .NET というのがあるんですね。補完も効いていい感じ。

image.png

英語ですが、これを使ったチュートリアルもありました。

Hello World - Introduction to C# interactive C# tutorial

本当にコンソールアプリで触ってみたいとかなら、これでもよさそう。

ローカルの VS Code で試してみたくて Web アプリも試したいという人は以下の VS Code + リモート開発をどうぞ。

Docker と VSCode を入れる

大体の人が既に入れてるよね?今回は、以下の 2 つが入ってることを前提に進めます。

VSCode のリモート開発拡張機能を入れる

まだ、プレビューなんですが Visual Studio Code のリモート開発拡張機能を入れます。

Remote Development

これを入れると Docker コンテナに繋いでホストマシンの VS Code でコード書いたりできます。

Hello world

  1. 適当な空のフォルダーを VS Code で開きます
  2. コマンドパレットから Remote-Containers: Add Development Container Configuration Files... を叩きます image.png
  3. 色々な言語などのリストが出てくるので C# .NET Core 3.1 を選びます image.png
  4. リモート開発用の構成ファイルが追加されます。
  5. .devcontainer/devcontainer.json"extensions":"ms-vscode.csharp" とありますが、これを "ms-dotnettools.csharp" に変更します。
    image.png

  6. コマンドパレットから Remote-Containers: Reopen in Container を実行します
       image.png

    • 初回は docker pull とか走るので時間がかかります。
  7. しばらくすると、コンテナーが立ち上がって VS Code のリモート開発機能でつながった状態になります

  8. ターミナルで dotnet -version とすると以下のような結果になるはずです。

   root@7d5755714aa7:/workspaces/VSCodeCSharp# dotnet --version
   3.1.100
  1. ターミナルで dotnet new console -o HelloWorld と打ち込んでプロジェクトを生成します。
  2. ターミナルで code -r HelloWorld を入力して HelloWorld フォルダーを開きます。
  3. コマンドパレットから .NET: Generate Assets for Build and Debug を実行するとデバッグやビルドに必要な tasks.jsonlaunch.json が追加されます。 image.png
  4. この状態で、F5 を押すとデバッグできます。 image.png

後は、C# を勉強するだけ!参考サイトは公式と、鉄板のサイトとしては岩永さんのサイトがあります。

この他にも Microsoft Learn という自己学習用のサイトにも C# のコースがあります。

この Microsoft Learn の C# のラーニングパスの何処かに変数宣言での var は使うな的なことが書いてありますが、個人的には var 使う派なので、そこは許せない。(個人の感想)

Web アプリもいけます

このコンテナ上で Web アプリの開発とデバッグも行けます。試しに ASP.NET Core Razor Pages でやってみましょう。
コンテナの作業フォルダーのルートに戻って以下のコマンドを実行します。

# dotnet new webapp -o HelloRazor

そして code -r HelloRazor コマンドでフォルダーを開きましょう。コンソールの時と同じ手順で tasks.jsonlaunch.json を追加します。そして、F5 で実行すると自動でポートフォワーディングの設定まで追加してくれて、ホストマシン側のブラウザーでコンテナー内の Web アプリが開きます。

image.png

まとめ

Visual Studio Code のリモート開発機能を使えばサクッと C# を始めることが出来ます。
とりあえず自前環境に .NET Core SDK は入れたくないけど、C# ちょっと触ってみたいなという人向けの 1 つの選択肢としてどうぞ!

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

C# で出来ること一覧

C# で出来ること一覧

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

つくるオーオース WEBAPI編

MVCが出来たのでWEBAPIを実装する事で、
https://qiita.com/namikitakeo/items/0de598b8e43eb5b1ff94

Client Credentials Grantと、
https://qiita.com/namikitakeo/items/0c283b2e5da55670c542

Resource Owner Password Credentials Grantを再現しようと思います。
https://qiita.com/namikitakeo/items/ea23adbc0b5c941ff0ed

Tokenコントローラーを作成します。
http://localhost:5000/op/token

Controllers/TokenController.cs
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using myop.Models;

namespace myop.Controllers
{
    public class AccessToken
    {
        public string access_token { get; set; }
        public int expires_in { get; set; }
        public string token_type { get; set; }
        public string scope { get; set; }
    }

    [Route("op/[controller]")]
    [ApiController]
    public class TokenController : ControllerBase
    {
        private readonly myopContext _context;
        string CLIENT_ID;
        string CLIENT_SECRET;
        string GRANT_TYPE;
        string SCOPE;
        string USERNAME;
        string PASSWORD;

        public TokenController(myopContext context)
        {
            _context = context;
        }

        // POST: op/token
        [HttpPost]
        public async Task<ActionResult<AccessToken>> doPost()
        {
            string body = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
            string[] p =  body.Split('&');
            for (int i=0; i<p.Length; i++){
                string[] values =  p[i].Split('=');
                switch(values[0])
                {
                    case "client_id":CLIENT_ID=values[1];break;
                    case "client_secret":CLIENT_SECRET=values[1];break;
                    case "grant_type":GRANT_TYPE=values[1];break;
                    case "scope":SCOPE=values[1];break;
                    case "username":USERNAME=values[1];break;
                    case "password":PASSWORD=values[1];break;
                }
            }
            var client = await _context.Clients.FindAsync(CLIENT_ID);
            if (client == null) {
                return null;
            }
            if (client.GrantType != GRANT_TYPE) {
                return null;
            }
            string t="openid";
            if (SCOPE != null) {
                string[] s =  SCOPE.Split(' ');
                for (int j=0; j<s.Length; j++){
                    if (s[j]!="openid" && client.AllowedScope.Contains(s[j])) t=t+" "+s[j];
                }
            }
            SCOPE=t;
            if (client.AccessType == "confidential") {
                if (client.ClientSecret != CLIENT_SECRET) return null;
                if (client.GrantType == "client_credentials") USERNAME="admin";
            } else if (client.AccessType == "public") {
                if (client.GrantType != "password") return null;
                if (CLIENT_SECRET != null) return null;
            } else {
                return null;
            }
            if (client.GrantType == "password") {
                var user = await _context.Users.FindAsync(USERNAME);
                if (user == null ) return null;
                if (user.Password != PASSWORD) return null;
            }
            var token = await _context.Tokens.FindAsync(USERNAME);
            if (token != null) {
                _context.Tokens.Remove(token);
                await _context.SaveChangesAsync();
            }
            string random = Guid.NewGuid().ToString("N").ToUpper();
            token=new Token {Id = USERNAME, AccessToken = random, ExpiresIn=60, TokenType="bearer", Scope = SCOPE, Iat=DateTime.Now};
            _context.Add(token);
            await _context.SaveChangesAsync();
            AccessToken access_token=new AccessToken {access_token = random, expires_in=60, token_type="bearer", scope = SCOPE};
            return access_token;
        }
    }
}

Client Credentials Grant動いているっぽい。

% curl -d "client_id=client2&client_secret=client2&grant_type=client_credentials&scope=openid address" http://localhost:5000/op/token

{"access_token":"57E63D3C3E7040F98D9FA4A8CE55EB2D","expires_in":60,"token_type":"bearer","scope":"openid address"}

Resource Owner Password Credentials Grant動いているっぽい。

% curl -d "client_id=client1&grant_type=password&username=user01&password=user01&scope=openid profile" http://localhost:5000/op/token

{"access_token":"408AF2B56C064A228D6C341F01BC77EF","expires_in":60,"token_type":"bearer","scope":"openid profile"}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人的に C# が向かないと思うこと

先日 @okazuki さんが C# で出来ること一覧 を書いていて、とても同意だった。C# 10年以上やってきて、その成長には満足しているし、対応ソリューションやプラットフォームは凄いものがあると思っています。

しかしこの記事では、あえて C# が逆に向かないことが何かを考えてみました。

あくまで個人の見解であり、同意できないことがあることは事前に同意してきます
C# Love なので、非常にバイアスがあります
他の言語そこまで知らないので、どの言語がいいという話はありません
普段 C# か Node.js で開発しているので、そもそも C# に向かないことを自分がやっていないだけというオチはあるかも

Hello World までが遠い

LINE イベントや外部ハッカソンなど、Microsoft の外で活動する際、C# は Hello World までの時間が Node.js や Python に比べて時間がかかります。理由はいくつかありますが、以下の通り:

IDE が必須
メモ帳でもできる!!とかいうのは詭弁(またはHENTAI)で、IDE 必須です。他の言語も同様であり、Visual Studio Code があるから良いよねという意見もありますが、本当に C# の威力を発揮するなら Visual Studio が欲しいので、ここで時間がかかります。VSCode もとても良いけどね!

追記: Web API やコンソールアプリなど作るだけなら Visual Studio Code 最高!リアルタイムテストとかエミュレーターでごにょごにょやり出すと、VS 欲しい!です。

追記: そしてさらに、レベルがあがると VSCode すら不要だと。。。

Runtime
.NET Framework/.NET Core/Xamarin/Unity など用途によって色々あるのが強みでもあり、複雑さを増している感があります。.NET 5 が解決してくれることを期待しつつ。尚、耳に挟んだ情報では Python もたまに 2 か 3 か問題がある模様。

コード量
最近は少なくなりましたが、Console.Write("Hello World"); 以外のコードがそこそこあり、まだオブジェクト指向であるが故にスコープで失敗しがちです。これはオブジェクト指向言語全般に言えるでしょう。

Windows
C# の問題ではありませんが、個人的にイベントやハッカソンで C# を選択する場合、Windows ユーザーが比較的多い場合で、ここにも隠れた課題が。

  • まだ Windows 7 ですー (別にいいんだけど、ハードが弱いことも多い)
  • 会社の PC で制限があってツールはいらなーい
  • VPN とセキュリティソフトが悪さ?してー

などなど IDE 入れることすらままならない時が散見されます。

Hello World の後も壁がそこそこある

Hello World を超えて次に進撃した場合にも、C# ならではの壁があります。

VS のテンプレートがリッチ
これはとても良い事だけど、初心者に対する敷居をすこし上げている気もします。過保護かな。。Node.js/express のように、とりあえず get だけ書こうねーという気楽さが足りない一方で、ユニットテストとか初めからついてきて最高とも言えますが、作成直後、ユニットテストプロジェクトがまず消される事件現場に何度も遭遇したことがあります。

サンプルが多様すぎる
C# は歴史は中堅どころですが、進化している言語なので、時期によりサンプルの書き方が大幅に異なります。これは他の言語でも当然言えるはずですが、数年離れると何書いてるかわからないから、ギャル語 (死語ですいません)のようだといわれたこともあります。(async/await、Linq、Generic、ラムダ、var の頃)

ただし、他言語のいい所は取り入れているので記述方式などに特殊さはあまりないと思っています。

実行環境の壁がある
C# は色々できる反面、実行環境が (まだ?) 1 つではありません。.NET Core/Framework、UWP、WPF など、フレームワークや実行環境が異なると、コピペで動かないものもあり、混乱を呼びます。C# というキーワードだけでなく、ラインタイムを意識する必要があるので、コピペ職人泣かせです。(職人までくると逆に大丈夫か)
また同じ Windows アプリでもものによって使える API が異なるため、同じく動かないコードがあります。こちらも極力解消するよう Microsoft は日々頑張っています

データクエリ

実は Spark でも動く我らが C#! (.NET for Apache® Spark™ を参照)
そしてデータクエリは Entity Framework が(賛否両論含めて)ある!

だが基本データ操作は SQL Server なら T-SQL がいいし、それぞれ専用のものがあるので、C# あればデータクエリも完璧だぜとは言い難いです。これは他の言語も同様だと認識してます。違ったら教えてください。

機械学習

ML.NET はじめました!なんですが、サンプルなど色々考えると、現状モデル読み込みと実行する時に、C# で出来るからアプリが C# でも安心!くらいの感じで、モデル開発中は Python やら R などをよく使います。サンプルも多いですし実行環境も多いので。この分野はこれからかな?

マイクロソフト製品が対応していない場所での利用

適材適所のため、MS もなんでもかんでも C#/.NET ではありません。Power BI のカスタムビジュアルや SharePoint Framework、PowerApps Component Framework などは Node.js です。そこに無理やり C# を使って Javascript コード生成をねじ込むのは、ベテランとツールを持っている人だけがやる事でしょう。

いまでは TypeScript もありますので、型問題はある程度解決です。

関数型プログラミング

C# はオブジェクト指向型のため向きません。F# をどうぞ。

スクリプト処理

Linux/Windows に限らず様々な自動化でスクリプトを書きますが、Windows であっても PowerShell で書きます。PowerShell は C# 実行できるとか、モジュールが C# で書ける!とかそういうのは関係なく、スクリプト処理に向く言語ではないと思います。

また az コマンドなど各種 CLI があるものは、当然そのまま使うので、わざわざ C# でラップして呼び出すようなことは(特に要件が無い限り)しません。

補足: dotnet-script なる C# でスクリプト実行する面白いライブラリがあることをコメントで教えていただきました。Azure Function ぽくていいかも!
そしてC#でもスクリプト実行したい!も併せてどうぞ。

Arudino や micro:bit

C# をねじ込む必要はありません。専用の言語と IDE でどうぞ。

コンソール環境のみで開発

無理ゲーです。そんなことする理由は不明ですが、開発はコンソールでしかしない!vim 最高!と言われたら C# を推すことはしていません。

まとめ

とりあえず色々思いついたことをつらつらと書いてみましたが、基本的には短期イベントの際に、経験者が少ない場合はお勧めしてません。

もし向いてないリストをもっと昔に書いたら、リストはこんなものでは無かったでしょう。

モバイル開発できない -> Mono と Xamarin
Windows でしか動かない -> .NET Core
ブラックボックスだ -> オープンソース化
ビルドから実行が遅い -> Roslyn
C# はそのうち終わる -> いわれ続けてもうじき 20 年。ありがとう。
クライアントサイドがごにょごにょ -> Blazor
その他山ほどの不満 -> フィードバックとして日々進化

結論: C# は投資に値する良い言語です。(趣旨と違

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

C#:列挙型の解説

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

// 列挙型とは?
// * コードを読みやすくするためのもの(可読性をあげる)
// * 制限の中から選択可能にするもの

public class Sample : MonoBehaviour
{
    enum DIRECTION {
        UP,
        DOWN,
        RIGHT,
        LEFT,
    }

    DIRECTION direction = DIRECTION.DOWN;
    int directionInt = 0; // 0:上

    void Start()
    {

        if (directionInt == 0)
        {
            Debug.Log("下");
        }
        else if(directionInt == 1)
        {
            Debug.Log("上");
        }

        switch (direction)
        {
            case DIRECTION.DOWN:
                Debug.Log("下");
                break;
            case DIRECTION.UP:
                Debug.Log("下");
                break;
            case DIRECTION.RIGHT:
                Debug.Log("下");
                break;
            case DIRECTION.LEFT:
                Debug.Log("下");
                break;
        }

        /*
        if (direction == DIRECTION.Down)
        {
            Debug.Log("下");
        }
        else if (direction == DIRECTION.Up)
        {
            Debug.Log("上");

        }
        else if (direction == DIRECTION.RIGHT)
        {
            Debug.Log("右");

        }
        else if (direction == DIRECTION.LEFT)
        {
            Debug.Log("左");
        }
        */

    }
}

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

C#:2次元配列の解説

スクリーンショット 2020-03-23 9.51.49.png

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

// 2次元配列
// * Table/表

public class Sample : MonoBehaviour
{
    // 宣言方法
    // 1次元配列
    int[] arrayInt = new int[3];
    // 2次元配列
    int[,] tabelInt = new int[3, 2];
    void Start()
    {
        // 代入方法
        tabelInt[0, 0] = 1;
        tabelInt[2,1] = 10;
        // 取得方法
        //int x = tabelInt[0, 0];

        //長さ
        //Debug.Log(arrayInt.Length);
        Debug.Log(tabelInt.GetLength(0));
        Debug.Log(tabelInt.GetLength(1));
        for (int x = 0; x<tabelInt.GetLength(0); x++)
        {
            for (int y = 0; y < tabelInt.GetLength(1); y++)
            {
                Debug.Log(tabelInt[x,y]);
            }
        }
        for (int y = 0; y < tabelInt.GetLength(1); y++)
        {
            for (int x = 0; x < tabelInt.GetLength(0); x++)
            {
                Debug.Log(tabelInt[x, y]);
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

つくるオーオース MVC編

はじめに

この記事では.NET Core 3.1をつかってオーオースを学習する方法を書いてみます。

実行環境

下記バージョンで動作確認しています。
- MacOS
- .NET Core 3.1

% dotnet --version
3.1.101

学習方針

コマンドプロンプトから実行する事で、Windows、Linuxにおいてもそのままできると思います。

% mkdir myop
% cd myop
% dotnet new mvc

必要なツールをインストールします。

% dotnet tool install --global dotnet-ef
% dotnet tool install --global dotnet-aspnet-codegenerator
% dotnet tool list --global

必要なパッケージをインストールします。

% dotnet add package Microsoft.EntityFrameworkCore.Sqlite
% dotnet add package Microsoft.EntityFrameworkCore.Design
% dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
% dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Clients、Tokens、Usersモデルを作成します。

Models/Tables.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace myop.Models
{
  public class myopContext : DbContext
  {
//        public myopContext (DbContextOptions options) : base(options) {}
        public DbSet<Client> Clients { get; set; }
        public DbSet<Token> Tokens { get; set; }
        public DbSet<User> Users { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite("Data Source=myop.db");
  }
  public class Client
  {
    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("client_secret")]
    public string ClientSecret { get; set; }

    [DisplayName("access_type")]
    public string AccessType { get; set; }

    [DisplayName("redirect_uri")]
    public string RedirectUri { get; set; }

    [DisplayName("grant_type")]
    public string GrantType { get; set; }

    [DisplayName("allowed_scope")]
    public string AllowedScope { get; set; }
  }

  public class Token
  {
    [DisplayName("user_id")]
    public string Id { get; set; }

    [DisplayName("access_token")]
    public string AccessToken { get; set; }

    [DisplayName("expires_in")]
    public int ExpiresIn { get; set; }

    [DisplayName("token_type")]
    public string TokenType { get; set; }

    [DisplayName("scope")]
    public string Scope { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class User
  {
    [DisplayName("user_id")]
    public string Id { get; set; }

    [DisplayName("password")]
    public string Password { get; set; }
  }
}

モデルからデータベースを生成します。今回データベースにはSQLiteを使います。

% dotnet ef migrations add InitialCreate
% dotnet ef database update

テスト用なのでポート番号は5000のみで良いと思います。

Properties/launchSetting.json
{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:38239",
      "sslPort": 44320
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "myop": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

スキャフォールドでデータベースを確認してみます。

% dotnet aspnet-codegenerator controller -name ClientsController -m Client -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

% dotnet aspnet-codegenerator controller -name TokensController -m Token -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

% dotnet aspnet-codegenerator controller -name UsersController -m User -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

ソースコードを一部修正して実行します。これでClients、Tokens、UsersがMVCで登録できます。
http://localhost:5000/Clients/
http://localhost:5000/Tokens/
http://localhost:5000/Users/

Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using myop.Models;
using Microsoft.EntityFrameworkCore;

namespace myop
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<myopContext>(options => options.UseSqlite("Data Source=myop.db"));
            services.AddControllersWithViews();
        }
(省略)
Models/Tables.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace myop.Models
{
  public class myopContext : DbContext
  {
        public myopContext (DbContextOptions options) : base(options) {}
        public DbSet<Client> Clients { get; set; }
        public DbSet<Token> Tokens { get; set; }
        public DbSet<User> Users { get; set; }
//        protected override void OnConfiguring(DbContextOptionsBuilder options)
//            => options.UseSqlite("Data Source=myop.db");
  }
  public class Client
  {
    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("client_secret")]
    public string ClientSecret { get; set; }

    [DisplayName("access_type")]
    public string AccessType { get; set; }

    [DisplayName("redirect_uri")]
    public string RedirectUri { get; set; }

    [DisplayName("grant_type")]
    public string GrantType { get; set; }

    [DisplayName("allowed_scope")]
    public string AllowedScope { get; set; }
  }

  public class Token
  {
    [DisplayName("user_id")]
    public string Id { get; set; }

    [DisplayName("access_token")]
    public string AccessToken { get; set; }

    [DisplayName("expires_in")]
    public int ExpiresIn { get; set; }

    [DisplayName("token_type")]
    public string TokenType { get; set; }

    [DisplayName("scope")]
    public string Scope { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class User
  {
    [DisplayName("user_id")]
    public string Id { get; set; }

    [DisplayName("password")]
    public string Password { get; set; }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UiPath Activity Creatorを使ってみる。(その5:スコープを持ったアクティビティの作成)

このドキュメントの概要

 前回 はActivity Creatorでのアクティビティの外観の設定方法について見ていきました。
 今回はスコープをもったアクティビティの作成方法について見ていきます。

WizardでのScope指定

 ActivityCreatorのWizardでACtivityの定義画面でType項目をScopeに設定するとScopeをもったアクティビティになります。また同一のWizard画面内でScopeを持ったアクティビティとSimple設定のアクティビティを作成すると、そのSimpleアクティビティはScopeアクティビティのChildアクティビティとして設定されます。
uac5-1.png
TestScopeアクティビティのプロパティを以下のように設定しました。
uac5-2.png

またTestChildアクティビティのプロパティを以下のように設定しました。
uac5-3.png

TestScopeアクティビティとして以下のコードが自動生成されます。

TestScope.cs
using System;
using System.Activities;
using System.Threading;
using System.Threading.Tasks;
using System.Activities.Statements;
using System.ComponentModel;
using Company.Product3.Activities.Properties;
using UiPath.Shared.Activities;
using UiPath.Shared.Activities.Localization;

namespace Company.Product3.Activities
{
    [LocalizedDisplayName(nameof(Resources.TestScope_DisplayName))]
    [LocalizedDescription(nameof(Resources.TestScope_Description))]
    public class TestScope : ContinuableAsyncNativeActivity
    {
        #region Properties

        [Browsable(false)]
        public ActivityAction<IObjectContainer> Body { get; set; }

        /// <summary>
        /// If set, continue executing the remaining activities even if the current activity has failed.
        /// </summary>
        [LocalizedCategory(nameof(Resources.Common_Category))]
        [LocalizedDisplayName(nameof(Resources.ContinueOnError_DisplayName))]
        [LocalizedDescription(nameof(Resources.ContinueOnError_Description))]
        public override InArgument<bool> ContinueOnError { get; set; }

        [LocalizedDisplayName(nameof(Resources.TestScope_Str_DisplayName))]
        [LocalizedDescription(nameof(Resources.TestScope_Str_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        public InArgument<string> Str { get; set; }

        // A tag used to identify the scope in the activity context
        internal static string ParentContainerPropertyTag => "ScopeActivity";

        // Object Container: Add strongly-typed objects here and they will be available in the scope's child activities.
        private readonly IObjectContainer _objectContainer;

        #endregion


        #region Constructors

        public TestScope(IObjectContainer objectContainer)
        {
            _objectContainer = objectContainer;

            Body = new ActivityAction<IObjectContainer>
            {
                Argument = new DelegateInArgument<IObjectContainer> (ParentContainerPropertyTag),
                Handler = new Sequence { DisplayName = Resources.Do }
            };
        }

        public TestScope() : this(new ObjectContainer())
        {

        }

        #endregion


        #region Protected Methods

        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            if (Str == null) metadata.AddValidationError(string.Format(Resources.ValidationValue_Error, nameof(Str)));

            base.CacheMetadata(metadata);
        }

        protected override async Task<Action<NativeActivityContext>> ExecuteAsync(NativeActivityContext  context, CancellationToken cancellationToken)
        {
            // Inputs
            var str = Str.Get(context);

            return (ctx) => {
                // Schedule child activities
                if (Body != null)
                    ctx.ScheduleAction<IObjectContainer>(Body, _objectContainer, OnCompleted, OnFaulted);

                // Outputs
            };
        }

        #endregion


        #region Events

        private void OnFaulted(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom)
        {
            faultContext.CancelChildren();
            Cleanup();
        }

        private void OnCompleted(NativeActivityContext context, ActivityInstance completedInstance)
        {
            Cleanup();
        }

        #endregion


        #region Helpers

        private void Cleanup()
        {
            var disposableObjects = _objectContainer.Where(o => o is IDisposable);
            foreach (var obj in disposableObjects)
            {
                if (obj is IDisposable dispObject)
                    dispObject.Dispose();
            }
            _objectContainer.Clear();
        }

        #endregion
    }
}

Childアクティビティへのデータの受け渡し

 ScopeアクティビティからChildアクティビティへのデータの受け渡しは、ActivityCreatorで用意しているobjectContainerを利用します。
 今回はScopeアクティビティにInArgument<string>型のプロパティStrがありますので、これをChildアクティビティでも使えるようにします。具体的には以下のように _objectContainerにAddメソッドでデータを追加します。

TestScope.cs
()
            // Inputs
            var str = Str.Get(context);
            _objectContainer.Add(str);
            return (ctx) => {
                // Schedule child activities
                if (Body != null)
                    ctx.ScheduleAction<IObjectContainer>(Body, _objectContainer, OnCompleted, OnFaulted);

                // Outputs
            };
()

TestChild側ではobjectContainerインスタンスに対してGet<T>で値を取得します。
以下の例ではTestScopeアクティビティのプロパティとして取得した値の前に"000"を付加してOutStringプロパティとして返しています。

TestChild.cs
()
       protected override async Task<Action<AsyncCodeActivityContext>> ExecuteAsync(AsyncCodeActivityContext context, CancellationToken cancellationToken)
        {
            // Object Container: Use objectContainer.Get<T>() to retrieve objects from the scope
            var objectContainer = context.GetFromContext<IObjectContainer>(TestScope.ParentContainerPropertyTag);

            // Inputs
            var str = objectContainer.Get<string>();

            ///////////////////////////
            // Add execution logic HERE
            ///////////////////////////
            str = "000" + str;

            // Outputs
            return (ctx) => {
                OutString.Set(ctx, str);
            };
        }
()

 動作確認のためUiPath Studioで以下のようなワークフローを作成してみます。
uac5-4.png

 TestChildには入力プロパティがありませんが、TestScopeで入力した値を内部で参照しています。そのためTestScopeの入力で仮に"123"とした場合、strOutputには"000123"が返ってきます。

Wizard作成後に別のアクティビティをChildアクティビティにする方法

 Scopeアクティビティを作成した後にChildアクティビティを追加したい場合があると思います。
 この場合はWizardでTypeをSimpleにしたアクティビティを作成した後、以下の設定を加えることによりChildアクティビティにすることができます。
1. using UiPath.Shared.Activities.Utilities; の追加
2. コンストラクタにScope内でのみの利用制限の追加
3. 実行部分にobjectContainerの定義の追加

後で追加したTestChild2アクティビティに対する具体的な変更個所は以下になります。

TestChild2.cs
()

using UiPath.Shared.Activities.Utilities;

(略)
        #region Constructors

        public TestChild2()
        {
            Constraints.Add(ActivityConstraints.HasParentType<TestChild2, TestScope>(string.Format(Resources.ValidationScope_Error, Resources.TestScope_DisplayName)));
        }

        #endregion

(略)
            // Inputs
            var objectContainer = context.GetFromContext<IObjectContainer>(TestScope.ParentContainerPropertyTag);

(略)

次回はnupkg周りの設定について説明します。

(その5 おわり)

その1 その2 その3 その4

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

再入門C#:デッドロック

コンカレントプログラミングの本を読んでいると、必ずデッドロックの話がでてくるので、実際にデッドロックするのか試したくなった。

デッドロックするコード

DeadLock() メソッドをシングルスレッドしか許されていない箇所から呼び出すとデッドロックが起こる。例えばWPFのUIスレッドから、これを呼び出すとデッドロックが起こる。理由は task.Wait() を呼び出すとこのスレッドのところで、待ちが発生する。一方、WaitAsAsync() メソッドの方では、await で、Task.Delay() を呼んでいる。await のセクションに入るときに現在のスレッドのコンテキストを保存する。await の箇所が終わると、そのコンテキストをリストアするのだが、その時にそのコンテキストは、task.Wait() によってロックがかかっているために、デッドロックになる。

        private async Task WaitAsAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1)); 
        }

        private void DeadLock()
        {
            Task task = WaitAsAsync();
            task.Wait();
        }

WPF アプリ

WPFは触ったことないけど、師匠のブログを読んで簡単なアプリを作ってみた。ボタンをおしていくと5回目で、Thread.Sleep() がかかり、10回目でこのデッドロックのロジックが呼ばれる。

MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new Prefecture();
        }

        private async Task WaitAsAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1)); 
        }

        private void DeadLock()
        {
            Task task = WaitAsAsync();
            task.Wait();
        }

        private int count = 0; 
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var button = (Button) sender;
            button.Content = string.Format($"{++count} times.");
            if (count == 4)
            {
                button.Content = string.Format($"Sleep 10 sec....");
            }
            if (count == 4)
            {
                Thread.Sleep(TimeSpan.FromSeconds(10));
            }

            if (count == 10)
            {
                button.Content = string.Format($"DeadLock....");
            }
            if (count == 11)
            {
                DeadLock();
            }
        }
    }
MainWindow.xaml
<Window x:Class="WPFSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition>
            </RowDefinition>
            <RowDefinition>
            </RowDefinition>
        </Grid.RowDefinitions>

    <ComboBox ItemsSource="{Binding Path=Data}" x:Name="comboBox" Grid.Row ="0" Grid.Column="0"/>
    <Button Content="0回" Grid.Row="1" Grid.Column="0" Click="Button_Click"/>
    </Grid>
</Window>

デッドロックに行く直前なら、プルダウンなどの操作できるが、一旦行ってしまうと何もできなくなる。

image.png

操作不能

image.png

回避方法

ConfigureAwait() を使うと、コンテキストのリストアをするか否かを制御できる。falseにすると、コンテキストをリストアして元のスレッドに戻ろうとしない(違うスレッドを使う)ので、デッドロックが起こらなくなる。

        private async Task WaitAsAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
        }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む