C#のActionクラスとFuncクラスを理解する
photo credit: ARMLE Action ! via photopin (license)
C#でメソッドや関数を表す 2つのクラス Action と Func について説明します (全くの余談ですが、C言語的には関数ポインタ、JavaScript的にはFunctionオブジェクトに相当するものです)。
2つの違いは戻り値の有無。戻り値なしが Action で、戻り値ありが Func です。
どちらもジェネリックパラメータとして関数のパラメータリストを指定できるジェネリッククラスです。 ジェネリックパラメータを指定しない場合は引数無しの関数を表すことになります。
以降、それぞれの詳細な使い方について説明します。
※ この記事は、もともと「C#のラムダ式はAction・Funcと一緒に理解を深めるとヨロシイようで」で書いていた内容です。長ったらしいのでこちらに独立させました。
Action クラス
Actionクラスは「戻り値のない処理を記述するためのクラス」です。
Action
だけなら引数もなし。
// 引数無しのActionをラムダ式で生成
Action action = () => {
foo.bar();
};
引数が必要ならAction<引数の型リスト>
を使用します。
例えば、
//引数を取るActionをラムダ式で生成 Action<string, int> action = (name, age) => { Console.WriteLine(string.Format( "{0} is {1} years old.", name, age)); };
てな感じです。
このように、インスタンスを作ること自体は難しいことではありませんが、誰かが作った既存のメソッドがActionクラスの引数を要求している場合、この逆の考え方、つまり、そのメソッドの定義を見て「何を与えればよいのか」をきちんと理解する必要がありますね。
覚えること ☞「Actionクラスは戻り値のない関数。ジェネリックパラメータは引数リストの型を表しています。」
Func<TResult>
クラス
Funcは戻り値があるメソッドを表します。戻り値がboolで、引数のないFuncは、以下のように記述します。
//戻り値がboolのFuncをラムダで生成 Func<bool> func = () => { return true; };
引数が必要な場合は、Actionと同じくジェネリックパラメータで型を指定します。
//戻り値がboolで引数付きのFuncをラムダで生成 Func<string, int, bool> isAround50 = (name, age) => Console.WriteLine(string.Format( "{0} is {1} years old.", name, age)); if(45 <= age && age < 55) { return true; } return false; };
これがたとえば、メソッドのパラメータだとしても、考え方は同じで、以下のように書くわけです。
Task<bool> task = new Task<bool>(() => { return execute.Invoke(); }); chainedTask._task.Start();
Lambda式は、ActionかFuncクラスのインスタンスを生成していると考えられます。 このため、Lambda式だけを見て、どちらの型なのか推測するクセをつけておくと、混乱が少なくなる気がします。 ようするにしっかり理解しておきましょうということですが。
上の例では、Task<bool>
のコンストラクタの第一引数の型は Func<bool>
だと推測できますね。
そのほか無駄話など
Func<void>
ではダメなんですか?
個人的な好みとしてですが、戻り値の有無でクラスを分けずに、Func<void>
を認めて、Action
クラスはなくてよいでしょと思っています。
しかし、そもそもジェネリックの型パラメータにvoid
は無理なのかもしれませんね。
VBでもFunctionとSubに分かれているし、マイクロソフトさんは昔から分けたい派なのかと思っていたけど、言語的制約なのかもしれません。
mocha を使った npm のユニットテストをブラウザで動かす設定
photo credit: wuestenigel White Cup with Coffee Grains via photopin (license)
mochaとchaiを使ったnpmのユニットテストをブラウザで動かすための設定をご紹介。
mochaにはブラウザで動作させる機能が備わっていますが、テストスクリプト以外にHTMLも用意しなくてはならないので少し億劫なんですよね。
実際には、ほぼ定型のHTMLなのですが、結構情報が少なくて「これで決まり!」みたいなのがない感じ。 コンソールとブラウザでスクリプトを共有したい場合に、問題が出て途方に暮れたこともありました。
ところが最近、ゆる~く試行錯誤を繰り返した結果、自分なりのテンプレートみたいなのが出来上がりつつあるので、ここに書いておきますよっと。
これとは逆に、WEBブラウザ向けESM(ESモジュール)のmochaのスクリプトをNode.jsでテストしたい場合の設定は、以下のページに書いています。
ブラウザでmochaのテストを行う為のHTML
以下はユニットテストをブラウザーで動かすために必要なHTMLファイルです。
test/web-test.html
<!DOCTYPE html> <html> <head> <title>npm run web-test</title> <link rel="stylesheet" href="../node_modules/mocha/mocha.css" /> </head> <body> <h1>npm run web-test</h1> <div id="mocha"></div> <!-- mocha の読み込みとセットアップ --> <script src="../node_modules/mocha/mocha.js"></script> <script> mocha.setup('bdd'); mocha.setup('tdd'); </script> <!-- テストスクリプトを読み込む --> <script src="./web-test.js"></script> <!-- テストの実行 --> <script> localStorage.debug = "*"; //npm debug向けのデバッグログの設定。 mocha.run(); </script> </body> </html>
mochaの公式ページに掲載されているHTMLファイル(→RUNNING MOCHA IN THE BROWSER - mocha)をもとにしており、npm の開発環境にインストール(npm install --save-dev mocha chai
)されているモジュールを利用するように変更しています。
元ファイルでは mocha と chai の外部スクリプトをCDNから読み込んでいますが、mocha は node_modules 以下のファイルを読み込むようにして、chai はJS側で取り込みます(理由は次項で)。
debug モジュールのすゝめ
必須ではありませんが、上の例では見やすいログを出力する debug
モジュールの設定をしています。
ブラウザではF12ツールのコンソールに色がついて見やすくなります。
WebではlocalStorage.debug
に、出力したいロガーの名前をカンマ区切りで指定しておきますが、上の例では "*"
としていますから、すべてのログが表示されます(mochaもdebugを利用しています)。
ブラウザで動作するmochaのテストスクリプト
chai は テストスクリプトで require する。
前述のように、元ファイルでは chai をSCRIPTタグで読み込んでいますが、自分的にはコンソールから実行するものと同じように、テストスクリプトで取り込むようにしています。 こうしておくことで、コンソールで実行するテストスクリプトのうち Node.js特有の機能を使っていない物をブラウザでも実行できるようになるからです。
しかし、これによってテストスクリプトをバンドラーでコンパイルしなくてはなりません。 これについては後述しますが、設定なしの Parcel を使えば簡単です。
ユーザー操作を待つテストの書き方
テストの実行時に、ボタンのクリックなどの操作を待つ必要がある場合は、その操作で解決する非同期関数を用意し、テストの中から呼び出しますようにします。 この場合、mochaのテストのタイムアウトを無効にしておく必要があります。
以下の例は、ボタンがクリックされてからテストが実行されます。
例)ユーザー操作を待つテストスクリプト
"use strict"; const assert = require("chai").assert; /** * ボタンを作って押されたら解決するPromiseを返す。 * Promiseは30秒後にリジェクトされる。 * @returns {Promise} Promiseを返す。 */ const buttonClicked = (() => (new Promise( (resolve, reject) => { const button = document.createElement("BUTTON"); button.innerHTML = "Click to go"; button.addEventListener("click", () => resolve() ); document.body.appendChild(button); setTimeout(() => { document.body.removeChild(button); reject(new Error("The user operation timeout")); }, 30000); }))); //テストターゲット const foo = () => "bar"; //ユニットテスト describe("foo", () => { it("should be bar", async () => {//非同期関数とします await buttonClicked(); //ボタンが押されるまで待つ関数 assert.equal( foo(), "bar" ); }).timeout(0); //タイムアウト無効 });
- 待ち時間が30秒を越えるとエラーを投入するのでテストは失敗します。
- クリックするためのボタンを動的に追加していますが、あらかじめHTMLに書いておいても構いません。
npmのscriptsを設定する
テストに限らず、何らかのコマンドの実行は npm の scripts
を使うと便利です。
node_modules 以下にあるモジュールのコマンドはパスの指定をせずに使えますから。
ここでは一例として、npm run web-test
でブラウザでのテストを実行するよう設定し、
npm test
または npm run test
で通常のコンソールでのテストを実行するようにしています。
この辺はお好みに応じて変更してください。
package.json(一部):
"scripts": { "test": "mocha", "web-test": "parcel test/web-test.html --open", ・・・ },
Parcel のすゝめ
ブラウザでのテストは、Parcel だけで 「バンドル」 → 「Webサーバー起動」 →「 ページを開く」 までを一気にやっています。 Parcelめっちゃ便利です。ブラウザーでのテストのためだけに使っても良いかも?って思っています。 BrowserifyやWeb-Packでは、いろいろ細かな設定が必要になるはずです。
ページを開いたときの実行時エラー 「regeneratorRuntime is not defined」を解消する
Parcelを使うとBabelも使うことになりますが、そのままだとブラウザでの実行時に以下のようなエラーが出ることがあります。
Uncaught ReferenceError: regeneratorRuntime is not defined at Suite.<anonymous> (test-script.js:38) at Object.create (mocha.js:720) at context.describe.context.context (mocha.js:532) at Suite.<anonymous> (test-script.js:7) at Object.create (mocha.js:720) at context.describe.context.context (mocha.js:532) at Suite.<anonymous> (test-script.js:6) at Object.create (mocha.js:720) at context.describe.context.context (mocha.js:532) at Object.parcelRequire.transworker.js.chai (test-script.js:5)
このエラーはBabelで変換対象となるような比較的新しいコードがあるときに発生するように思います。
解消するには、以下のような babel-polyfill だけをインポートするスクリプトを用意して、HTMLのSCRIPTで、テストスクリプトよりも前に読み込みます。ポリフィルはWebページから一度だけ参照すべきなので、このような対処になります。
test/babel-polyfill.js
require("babel-polyfill");
test/web-test.html
<!-- ポリフィルを読み込む --> <script src="./babel-polyfill.js"></script> <!-- テストスクリプトを読み込む --> <script src="./web-test.js"></script>
コンソールでブラウザ向けテストを除外する
細かなことですが、コンソールでもテストを行っている場合は、 ブラウザ特有の機能を使っているテストスクリプトを除外してやる必要があります。
これは、mocha.opt というファイルで行います。 以下の例では、testディレクトリ内のファイル名が'web-'で始まるファイルを除外しています。
test/mocha.opt
test/!(web-)*.js
リンク
あなたが正しくウェブページのカーソル形状を変更するには
photo credit: Maria Eklind Frankie & Benjys bookstore and theatre Reggiano via photopin (license)
なにやら暑苦しいタイトルで失礼します。
ここにはウェブページのマウスカーソルの形状を変更するにはどうすればってことを書いてます。
「何を今さら」って感じですけど、先日、目からウロコが3つほど落ちましたので書いておきます。
小ネタといえば小ネタです。
ページ全体のカーソル形状を変更する
ページ全体でマウスカーソルの形状を「待ち状態」にしたいことがありますね。 勝手に手が動くぐらいの勢いで以下のようにしていましたし、ググってみてもこのように説明されてるページが多いです。
でも、これ厳密には不完全なんですよ。
document.body.style.cursor = "wait";
何がダメかって言うと、WEBページのコンテンツが短い場合です。
コンテンツが少ないと、画面の下のほうにカーソル形状が変化しない領域が残ってしまうのです。
BODY要素(=document.body
)はブラウザ画面の全体に広がっているとは限らないということです。
スタイルシートがどうなってるかにもよりますけどね。
で、これを回避するのは実に簡単。BODYじゃなくてHTMLのスタイルを変更します。
document.body.parentElement.style.cursor = "wait";
document.bodyの親要素はHTMLです。document.documentElement
もHTML要素を指すそうですが、ブラウザ間の実装に違いがあるかもしれないので注意が必要。
以下のボタンを押すとHTMLのカーソル形状を変化させます。
コントロールは親要素のカーソル形状を継承しない
下のフォームのボタンを押すとBODYのカーソル形状をWaitカーソルに変えます(5秒後に元に戻ります)が、 各コントロールの上へカーソルを持って行っても形状は変化していません。 ChromeやFirefoxではLABELのカーソル形状も変化しません(Edgeでは変化しました)。
既定の形状に戻すのはdefault
ではない
一時的に変化させたカーソル形状を元に戻す場合、default
に設定しなおしていましたが、これでは全てが矢印になっていました。
ほとんどの要素の既定の形状は矢印ですが、そうでないのもありまして。
例えばテキストボックス(<input type="text"/>
)は縦線みたいなのが既定の形状。
const foo = getElementById("foo"); foo.style.cursor = "wait"; (長めの処理) foo.style.cursor = "default"; //← 全部矢印になっちゃいます
といっても、すべての要素の既定のカーソル形状を覚えておいて、それぞれ個別に元に戻すなんてナンセンス。
じゃ、どうするか。default
じゃなくauto
にすればよいのです。
const foo = getElementById("foo"); foo.style.cursor = "wait"; (長めの処理) foo.style.cursor = "auto"; // ← 要素ごとの既定のカーソルに戻ります。
ちなみに、最近のブラウザでは initial でも元に戻っているようです。
これはHTML5で規定されているのかもしれませんが、IE11では効きませんから、やっぱり auto
でよいみたいです。
default / initial / auto でそれぞれ意味が違うのでしょうから、ややこしいですね。
EdgeがIMGタグのSVGをストレッチしてくれないことがある
HTMLのIMGタグでSVG画像を表示するとき、EdgeではSVG画像がIMGタグのサイズに拡大・縮小されず、SVGで指定されたサイズでそのまま表示されることがあります。
他のブラウザ(Chrome、Firefox、Safari)では大丈夫でした。 また他の画像形式ならばEdgeでも問題ありません。
どうやらEdgeでは、SVGのサイズ指定方法によって表示が変わるようです。これが不具合なのかどうなのかはわかりません。
このようにEdgeはちょっと邪魔くさいことが多く、ブラウザシェアも6%程度(2018年9月現在の日本国内シェア)のようなので、できれば無視したいのですが、Windows 10の標準バンドル品なので、無視しがたいのが悩み所・・・。
※ 本記事で確認しているEdgeのバージョンは42。レンダリングエンジンEdgeHTMLのバージョンは17。
SVG関連の記事:
発生する現象
下の画像は、9×9のクロスハッチで、SVGのサイズは1215×1215pxのSVG画像です。 Edge以外では正しく表示されているはず。
gray-9x9-crosshatch-1215x1215.svg:
<img width="270px" src="https://takamin.github.io/images/gray-9x9-crosshatch-1215x1215.svg"/>
Edgeでは下のように表示されてしまいます。 IMGタグの領域内にオリジナルのスケールで表示されています。
原因
SVGファイル内での画像サイズの指定がstyle属性で行われていると、(Edgeだけで)この現象が発生するようです。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" style="width:1215px; height:1215px;" viewBox="0 0 321.46874 321.46876" version="1.1" . . .
対策
SVGファイルのSVG要素のサイズ指定をstyleではなくwidthと height で行えばEdgeでも正しく表示してくれます。
※ widthとheight属性でサイズ指定していても、styleでサイズが指定されていれば、スケーリングされないようです。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="1215px" height="1215px" viewBox="0 0 71.437498 71.437502" version="1.1" . . .
以下は属性だけでサイズ指定したSVGです。Edgeでも正しく表示されているはずですよ。
<img width="270px" src="https://takamin.github.io/images/gray-9x9-crosshatch-1215x1215-edge.svg"/>
他の対策
他の対策もありますので以下に書いておきますね。
style属性を書き換える
SVGのstyle属性でサイズを指定していても、表示したいサイズに変更すれば問題ありません。 しかしこれだと、大きなサイズの画像を縮小してサムネイル的に表示したいときなどに、ファイルを分ける必要があり、SVGを使っている利点が出ません。
XHRでSVGを取り出して表示。
外部サイトのSVGファイルなどで編集できない場合などは、別途XHRでSVGを取り出して内容を編集してから表示する必要があるかもしれません。 この場合は、IMGタグではなくインラインSVG(SVGタグ)で表示する手もあります。
何が正しいのかよくわからん
この挙動が、HTMLの仕様として正しいのかそうでもないのかは読み取れませんでした。 しかし、SVGの仕様では、style属性の中でwidthやheightを指定できるという記述は見つけられませんでした。
つまり、本来widthやheightはstyleで指定するものではないけれど、Edgeはそれを(良かれと思って?)解釈してくれて、上記のような挙動になっているのかもしれません。
リンク
ウェブアプリからGoogle Driveのファイルを扱う 'gdrive-fs'
photo credit: suzyhazelwood DSC04299-02 via photopin (license)
ウェブアプリから Google Drive のファイルを操作するモジュール(npm)を公開しましたので、ご紹介。
npmはこちら。
関連記事:
機能概要
サインインしたユーザーのファイルを読み書き
このモジュールを利用するウェブアプリで、ユーザーがGoogleアカウントでサインインすると、ウェブアプリからユーザー自身のGoogle Driveのファイルへアクセスできます。
これらのファイルは、ユーザーが特別に共有設定などをしない限り、他人には見ることができません(ただしアプリはDrive内の全内容を把握できることには注意が必要)。
ファイル名によるファイルの操作
ローカルのファイルシステムと同じように、ファイル名とフォルダ名によってファイルを扱うAPIを提供しています。
Google Drive API は、ファイルIDによってファイルにアクセスしますが、パスでアクセスできるほうが直感的ですよね。
各APIは、おおまかに Node.jsの fs モジュールのメソッド名に従っています。 ただし、多くが非同期関数として動作しますが、callback は受け付けず、Promiseを返します(ほとんどが async 関数です)。
UI作成支援機能
こちらはまだα版。動作確認用のサンプル実装程度です。
そのうちまじめに作って公開するつもりなのでちょっと待ってネ。
インストールと使い方
npmで公開しているので、npmでinstallしてください。
$ npm install --save gdrive-fs
バンドラーを使われているなら、CommonJS的にインポートしてください。
const Gdfs = require("gdrive-fs"); (async()=>{ await Gdfs.loadApi(<clientId>, <apiKey>); . . . })();
バンドラーを使っていない場合、HTMLのScriptタグで 本モジュールの/build/gdrive-fs.min.js
を読み込んでください。
この場合、グローバル変数として、Gdfsが利用できます。
<!DOCTYPE html> <html> <head>...</head> <body> . . . <script src="<path-to-module>/build/gdrive-fs.min.js"> (async()=>{ await Gdfs.loadApi(<clientId>, <apiKey>); . . . })(); </script> </body> </html>
動作条件
「APIキー」と「クライアント ID」
ウェブアプリ開発者は、Google Developer Console でプロジェクトを作成し、ウェブアプリケーション向けの認証情報として、「APIキー」と「OAuth 2.0のクライアント ID」を作成し、適切に設定しておく必要があります。
多くの場合、APIキーはHTTPリファラーによる制限をかけ、クライアントIDのタイプは「ウェブアプリケーション」としておきます。
APIリファレンス
本モジュールが提供しているクラス、APIの完全な説明は、以下のリンクからどうぞ。 つたない英文で書いています。
以下に要約した内容を転載します。最新情報は上記URLを参照してください。
Gdfsクラス
このクラスは Google Drive API v3 へのインターフェースであり、インスタンスはカレントディレクトリを管理して、ファイルやフォルダを操作するメソッドを提供します。
このインスタンスを作成する前に、これを利用するアプリケーションの Client Idと Api Keyにより、クラスメソッド loadApi
で、このAPI群が読み込まれていなければなりません。
これら2つのキーは、Google Developer Consoleのプロジェクトで作成されていなくてはなりません。
また、ファイルを操作するにはユーザーがGoogleアカウントでサインインしておく必要があります。
このインスタンスのカレントディレクトリは、コンストラクタでルートフォルダに初期化されます。
これはchdir
メソッドで変更できます。
カレントディレクトリ変更時には、oncwdupdate
コールバックが呼ばれます。カレントディレクトリを知るには、cwd
メソッドが利用できます。
クラスメソッド
async loadApi(clientId, apiKey)
- Google Drive API (v3)を読み込む。isSignedIn()
- Google Driveにサインインしているかどうかの確認。async signIn()
- Google Driveにサインインする。async signOut()
- Google Driveからサインアウトする。
コンストラクタ
Gdfs()
- コンストラクタ
インスタンスメソッド
async chdir(directory:string)
- カレントディレクトリの移動。cwd()
- カレントディレクトリを返す。async isDirectory(path:string)
- パスがディレクトリかどうかを返す。async mkdir(path:string)
- ディレクトリの作成。async readdir(path:string, options:object)
- ディレクトリ内のファイルのリストを読み出す。async readFile(path:string)
- ファイル内容の読み取り。async rmdir(path:string)
- ディレクトリ削除。async stat(path:string)
- ファイルのメタ情報を取得する。async unlink(path:string)
- ファイルの削除(ゴミ箱には入らない)async writeFile(path:string, mimeType:string, data:any)
- ファイルへの書き込み。または新規作成。
制限事項・注意事項
名前だけではファイルを特定できない問題は無視
Google Driveでは、一つのフォルダ内に同じ名前のファイルが複数存在できます。 なので、本当は名前だけではファイルを一意に特定できません。 でも、そういう使い方自体が混乱のもとですから、本モジュールではバッサリ無視して最初に見つけたほうを採用しています(多分)。
削除してもゴミ箱には入りません
Gdfs.unlink(path)
メソッド で削除した場合、ごみ箱には入りませんので、重々気を付けてくださいね。
セキュリティ警告とアプリケーションの申請について
未申請のアプリケーションに、ユーザーがサインインしようとすると「安全でないアプリケーションである」との警告が表示されます。 Googleにアプリケーションを申請して承認されていれば、これは表示されなくなるはずです。 アプリケーションを正式に公開する場合は申請しておいたほうが良いでしょう。
アプリケーションの申請はそれぞれのエンドポイントで行う必要があるため、本モジュールで行うものではありません。
ライセンス
本モジュールはMITライセンスにおいて公開しています。
免責
本モジュールは、これを利用するアプリケーションの安全性を保証せず、その動作について一切の責任を負いません。 本モジュールを利用するアプリケーションのすべての動作は、アプリケーションの製造者の責任において試験され、ユーザーに提供されるべきです。 悪意のあるアプリケーションや不具合によって被害を受けないためには、信頼できる人以外が作ったアプリケーションには接続するべきではありません。