- 投稿日:2019-05-06T19:44:38+09:00
R, Python, Juliaの性能比較
はじめに
- 「ガウス過程に基づく分類モデル」で行った計算を、R, Python, Juliaで実装して処理時間を比較。
- ついでにC#とC++でも実装して比較してみた。
- 事後分布をラプラス近似して、周辺尤度を最大化するパラメータを求める。
- 最適化手法は、勾配情報の不要なNelder-Meadアルゴリズムを採用。
- Windows 10 Pro, 16.0GBメモリ, i7-8550U CPU @ 1.80GHz
- The Julia Programming Language
- JuliaNLSolvers/Optim.jl
- SciPy
- Accord.NET Framework
- Math.NET Numerics
- Eigen: C++ template library for linear algebra
結果
評価関数の呼び出し回数を考慮すると、Juliaが最速で、その後にC++、Python、R、C#と続く。
言語環境 秒数 呼び出し回数 平均秒数 比率 Julia(1.1.0) 10.86 70 0.155 1 C++(VS2017) 8.51 47 0.181 1.17 Python(3.6.7/WSL) 13.06 59 0.221 1.43 R(3.5.3) 38.6 47 0.822 5.3 C#(VS2017) 47.6 47 1.013 6.5 Pythonで測定したところ、CPUの利用率が100%となり、24秒くらいかかった。
OMP_NUM_THREADS=1
として使用するスレッド数を1に制限したら速くなった。
C#が遅すぎるが、Install-Package MathNet.Numerics.MKL.Win-x64
として、
Intel Math Kernel Library (MKL)を使用すると劇的に速くなる。
言語環境 秒数 呼び出し回数 平均秒数 比率 C+++MKL 4.58 47 0.097 0.63 C#+MKL 6.59 47 0.140 0.90 R Open(3.5.2)+MKL 7.51 47 0.160 1.03 Python(3.7.3)+MKL 10.9 59 0.184 1.19 Microsoft R Openは、デフォルトでMKLを使用する。使用スレッド数を1に制限。
MinicondaでPythonをインストールするとnumpy+MKLを使用するため速くなる。
最速はC++/Eigen/MKL。
Julia+MKLは未評価。MKLを使用するためにはソースからコンパイルする必要あり。処理の大部分は行列(767x767)のコレスキー分解であり、MKLの効果が大きい。
サンプルデータ
USPS handwritten digit data
http://www.gaussianprocess.org/gpml/data/R
## データの入力 library("R.matlab") mat_data <- readMat('usps_resampled.mat') index3 = (mat_data$train.labels[3+1,] == 1) index5 = (mat_data$train.labels[5+1,] == 1) y_train = ifelse(index3,1,-1)[index3 | index5] x_train = mat_data$train.patterns[,index3 | index5] ## カーネル関数とシグモイド関数 x_train_L2 = apply(x_train,2,function(a){colSums((x_train-a)^2)}) my_kernel_train = function(k_sigma,k_scale){ exp(x_train_L2 / (-2 * k_scale^2)) * k_sigma^2 } my_sigmoid = function(y,f){ pnorm(y * f) } my_sigmoid_log_1st = function(y,f,prob){ y * dnorm(f) / prob } my_sigmoid_log_2nd = function(y,f,prob){ a = dnorm(f) / prob a * (a + y * f) } ## 周辺尤度の最大化 my_evidence_optim = function(par){ k_scale = exp(par[1]) k_sigma = exp(par[2]) K11 = my_kernel_train(k_sigma,k_scale) n = length(y_train) f = seq(from=0,to=1,length.out=n) while(TRUE){ prob = my_sigmoid(y_train,f) V1 = my_sigmoid_log_1st(y_train,f,prob) V2 = my_sigmoid_log_2nd(y_train,f,prob) V2q = sqrt(pmax(V2,0)) B = diag(n) + t(K11 * V2q) * V2q U = chol(B) # t(U) %*% U b = V2 * f + V1 a = b - backsolve(U,forwardsolve(t(U),(K11 %*% b) * V2q)) * V2q f0 = f f = drop(K11 %*% a) if(max(abs(f - f0))<1e-5)break } -sum(a * f) / 2 + sum(log(prob)) - sum(log(diag(U))) } start = proc.time() ret_optim = optim(c(2.85,2.35),my_evidence_optim,control=list(fnscale=-1)) proc.time() - start ret_optimJulia
Rとは異なり、
while
でも変数のスコープが構成される。
sum()
したら次元をドロップして欲しい。using MAT using Distributions using Optim using LinearAlgebra ## データの入力 matfile = matopen("usps_resampled.mat") train_labels = read(matfile, "train_labels") train_patterns = read(matfile, "train_patterns") index3 = (train_labels[3+1,:] .== 1) index5 = (train_labels[5+1,:] .== 1) y_train = ifelse.(index3,1,-1)[index3 .| index5] x_train = train_patterns[:,index3 .| index5] ## カーネル関数とシグモイド関数 x_train_L2 = mapslices(t -> dropdims(sum((x_train .- t) .^ 2, dims=1), dims=1), x_train, dims=1) function my_kernel_train(k_sigma,k_scale) exp.(x_train_L2 / (-2 * k_scale^2)) * k_sigma^2 end function my_sigmoid(y,f) cdf.(Normal(), y .* f) end function my_sigmoid_log_1st(y,f,prob) y .* pdf.(Normal(), f) ./ prob end function my_sigmoid_log_2nd(y,f,prob) a = pdf.(Normal(), f) ./ prob a .* (a .+ y .* f) end ## 周辺尤度の最大化 function my_evidence_optim(par) k_scale = exp(par[1]) k_sigma = exp(par[2]) K11 = my_kernel_train(k_sigma,k_scale) n = length(y_train) f = Array(range(0,1,length=n)) evidence = 0 while true prob = my_sigmoid(y_train,f) V1 = my_sigmoid_log_1st(y_train,f,prob) V2 = my_sigmoid_log_2nd(y_train,f,prob) V2q = sqrt.(max.(V2,0)) B = I + K11 .* V2q .* V2q' C = cholesky(Symmetric(B)) b = V2 .* f + V1 a = b - (C.U \ (C.L \ ((K11 * b) .* V2q))) .* V2q f0 = f f = K11 * a if maximum(abs.(f - f0)) < 1e-5 evidence = -dot(a,f) / 2 + sum(log.(prob)) - log(det(C)) / 2 break end end evidence end @time result = maximize(my_evidence_optim, [2.85,2.35], NelderMead())Python
np.array
とnp.matrix
の二種類があり挙動が異なる。import os os.environ["OMP_NUM_THREADS"] = "1" import time import numpy as np from scipy import io from scipy.stats import norm from scipy.linalg import cholesky from scipy.linalg import solve_triangular from scipy.optimize import minimize ## データの入力 matdata = io.loadmat('usps_resampled.mat') train_labels = matdata["train_labels"] train_patterns = matdata["train_patterns"] index3 = (train_labels[3,:]==1) index5 = (train_labels[5,:]==1) y_train = np.where(index3,1,-1)[np.logical_or(index3,index5)] x_train = train_patterns[:,np.logical_or(index3,index5)] ## カーネル関数とシグモイド関数 x_train_L2 = np.apply_along_axis(lambda x: np.sum(np.power(x_train-x.reshape(-1,1),2),0),0,x_train) def my_kernel_train(k_sigma,k_scale): return np.exp(x_train_L2 / (-2 * k_scale * k_scale)) * (k_sigma * k_sigma) def my_sigmoid(y,f): return norm.cdf(y * f) def my_sigmoid_log_1st(y,f,prob): return y * norm.pdf(f) / prob def my_sigmoid_log_2nd(y,f,prob): a = norm.pdf(f) / prob return a * (a + y * f) ## 周辺尤度の最大化 def my_evidence_optim(par): k_scale = np.exp(par[0]) k_sigma = np.exp(par[1]) K11 = my_kernel_train(k_sigma,k_scale) n = len(y_train) f = np.linspace(0,1,n) evidence = 0 while True: prob = my_sigmoid(y_train,f) V1 = my_sigmoid_log_1st(y_train,f,prob) V2 = my_sigmoid_log_2nd(y_train,f,prob) V2q = np.sqrt(np.maximum(V2,0)) B = np.identity(n) + K11 * V2q * V2q.reshape(-1,1) U = cholesky(B) b = V2 * f + V1 a = b - solve_triangular(U,solve_triangular(U,np.dot(K11,b) * V2q,trans=1)) * V2q f0 = f f = np.dot(K11,a) if np.max(np.abs(f - f0)) < 1e-5: evidence = -np.dot(a,f) / 2 + np.sum(np.log(prob)) - np.sum(np.log(np.diag(U))) break return -evidence start = time.time() par = np.array([2.85,2.35]) ret = minimize(my_evidence_optim,par,method='Nelder-Mead') print(time.time() - start) print(ret)C#
.mat
ファイルの入力にはAccord.Math
を使用。
行列計算と最適化にはMathNet.Numerics
を使用。
データの抽出と結合に少し手間がかかる。class BinaryHandwrittenDigit { (Vector<double>, Matrix<double>) Select(Int16[,] labels, Double[,] patterns) { int nrow = patterns.GetLength(0); int ncol = patterns.GetLength(1); int count_3 = 0; int count_5 = 0; for (int j = 0; j < ncol; j++) { if (labels[3, j] == 1) count_3++; else if (labels[5, j] == 1) count_5++; } Vector<double> y = new DenseVector(count_3 + count_5); Matrix<double> x = new DenseMatrix(nrow, count_3 + count_5); int i = 0; for (int j = 0; j < ncol; j++) { if (labels[3, j] == 1) { for (int k = 0; k < nrow; k++) x[k, i] = patterns[k, j]; y[i++] = 1; } else if (labels[5, j] == 1) { for (int k = 0; k < nrow; k++) x[k, i] = patterns[k, j]; y[i++] = -1; } } return (y, x); } Vector<double> y_train; Matrix<double> x_train; void ReadMat() { string path_to_file = "usps_resampled.mat"; var reader = new MatReader(path_to_file); var train_labels = reader.Read<Int16[,]>("train_labels"); var train_patterns = reader.Read<Double[,]>("train_patterns"); (y_train, x_train) = Select(train_labels, train_patterns); } Matrix<double> x_train_L2; Matrix<double> MakeMatrixL2(Matrix<double> x) { int m = x.ColumnCount; var result = new DenseMatrix(m, m); for (int j = 0; j < m; j++) { var cj = x.Column(j); for (int i = 0; i < m; i++) { var s = x.Column(i) - cj; result[i, j] = s * s; } } return result; } Matrix<double> kernel_train(double k_sigma, double k_scale) { return (x_train_L2 / (-2 * k_scale * k_scale)).PointwiseExp() * (k_sigma * k_sigma); } Vector<double> sigmoid(Vector<double> y, Vector<double> f) { return y.PointwiseMultiply(f).Map((x) => Normal.CDF(0, 1, x)); } Vector<double> sigmoid_log_1st(Vector<double> y, Vector<double> f, Vector<double> prob) { var df = f.Map((x) => Normal.PDF(0, 1, x)); return y.PointwiseDivide(prob).PointwiseMultiply(df); } Vector<double> sigmoid_log_2nd(Vector<double> y, Vector<double> f, Vector<double> prob) { var df = f.Map((x) => Normal.PDF(0, 1, x)); var a = df.PointwiseDivide(prob); return a.PointwiseMultiply(a + y.PointwiseMultiply(f)); } Matrix<double> bXbPlusOne(Matrix<double> X, Vector<double> b) { int n = X.RowCount; var result = X.Clone(); for (int j = 0; j < n; j++) { for (int i = 0; i < n; i++) { result[i, j] *= b[i] * b[j]; } result[j, j] += 1; } return result; } double GetEvidence(Vector<double> par) { double k_scale = Math.Exp(par[0]); double k_sigma = Math.Exp(par[1]); var K11 = kernel_train(k_sigma, k_scale); int n = y_train.Count; Vector<double> f = new DenseVector(n); for (int i = 0; i < n; i++) { f[i] = 1.0 / (n - 1) * i; } double evidence = 0; while (true) { var prob = sigmoid(y_train, f); var V1 = sigmoid_log_1st(y_train, f, prob); var V2 = sigmoid_log_2nd(y_train, f, prob); var V2q = V2.PointwiseMaximum(0).PointwiseSqrt(); var B = bXbPlusOne(K11, V2q); var b = V2.PointwiseMultiply(f) + V1; var c = K11.Multiply(b).PointwiseMultiply(V2q); var chol = B.Cholesky(); var a = b - chol.Solve(c).PointwiseMultiply(V2q); var f0 = f; f = K11 * a; if ((f - f0).AbsoluteMaximum() < 1e-5) { evidence = -(a * f) / 2 + prob.PointwiseLog().Sum() - Math.Log(chol.Determinant) / 2; break; } } return -evidence; } public void MarginalLikelihoodMaximization() { ReadMat(); x_train_L2 = MakeMatrixL2(x_train); var evidence_optim = ObjectiveFunction.Value(GetEvidence); Stopwatch sw = Stopwatch.StartNew(); var ret = NelderMeadSimplex.Minimum(evidence_optim, new DenseVector(new[] { 2.85, 2.35 })); sw.Stop(); Console.WriteLine("{0}sec", (double)(sw.ElapsedMilliseconds) / 1000); var par_m = ret.MinimizingPoint; Console.WriteLine("par={0},{1}", par_m[0], par_m[1]); Console.WriteLine("Value={0},Reason={1},Iterations={2}", ret.FunctionInfoAtMinimum.Value, ret.ReasonForExit, ret.Iterations); } } static void Main(string[] args) { Control.MaxDegreeOfParallelism = 1; var sample = new BinaryHandwrittenDigit(); sample.MarginalLikelihoodMaximization(); }C++
データは、C#で出力したものを入力。省略。
最適化関数nmmin
は、Rのソース/src/appl/optim.c
を流用して作成。省略。
Eigenでauto
を使用すると型が変わってしまう。MKLを使用する場合は、
#define EIGEN_USE_BLAS
として、
mkl_intel_lp64.lib,mkl_sequential.lib,mkl_core.lib
をリンクする。
Intel® Math Kernel Library Link Line Advisor#include <stdio.h> #include <direct.h> #include <chrono> #include <cmath> //#define EIGEN_USE_BLAS #include "Eigen/Dense" using namespace std; using namespace Eigen; typedef double optimfn(int n, const double *x0, void *ex); void nmmin(int n, double *Bvec, double *X, double *Fmin, optimfn fminfn, int *fail, double abstol, double intol, void *ex, double alpha, double bet, double gamm, int trace, int *fncount, int maxit); double Normal_PDF(double value) { const double pi = 3.1415926535897932384626433832795; return exp(-value * value / 2) / sqrt(2 * pi); } double Normal_CDF(double value) { return erfc(-value / sqrt(2)) / 2; } class BinaryHandwrittenDigit { private: int nrow; int ncol; MatrixXd x_train; ArrayXd y_train; MatrixXd x_train_L2; public: void read_data(); void make_matrix_L2() { x_train_L2.resize(ncol, ncol); for (int j = 0; j < ncol; j++) { for (int i = 0; i < ncol; i++) { double t = 0; for (int k = 0; k < nrow; k++) t += (x_train(k, i) - x_train(k, j)) * (x_train(k, i) - x_train(k, j)); x_train_L2(i, j) = t; } } } MatrixXd kernel_train(double k_sigma, double k_scale) { return (x_train_L2 / (-2 * k_scale * k_scale)).array().exp() * (k_sigma * k_sigma); } ArrayXd sigmoid(const ArrayXd& y, const ArrayXd& f) { ArrayXd result(y.size()); for (int j = 0; j < y.size(); j++) { result(j) = Normal_CDF(y(j) * f(j)); } return result; } ArrayXd sigmoid_log_1st(const ArrayXd& y, const ArrayXd& f, const ArrayXd& prob) { ArrayXd result(y.size()); for (int j = 0; j < y.size(); j++) { result(j) = y(j) * Normal_PDF(f(j)) / prob(j); } return result; } ArrayXd sigmoid_log_2nd(const ArrayXd& y, const ArrayXd& f, const ArrayXd& prob) { ArrayXd result(y.size()); for (int j = 0; j < y.size(); j++) { double a = Normal_PDF(f(j)) / prob(j); result(j) = a * (a + y(j) * f(j)); } return result; } MatrixXd bXbPlusOne(const MatrixXd& X, const ArrayXd& b) { int n = X.rows(); MatrixXd result(X); for (int j = 0; j < n; j++) { for (int i = 0; i < n; i++) { result(i, j) *= b[i] * b[j]; } result(j, j) += 1; } return result; } double get_evidence(const double* par) { double k_scale = exp(par[0]); double k_sigma = exp(par[1]); MatrixXd K11 = kernel_train(k_sigma, k_scale); int n = y_train.size(); ArrayXd f = ArrayXd::LinSpaced(n, 0, 1); double evidence = 0; while (true) { ArrayXd prob = sigmoid(y_train, f); ArrayXd V1 = sigmoid_log_1st(y_train, f, prob); ArrayXd V2 = sigmoid_log_2nd(y_train, f, prob); ArrayXd V2q = V2.cwiseMax(0.0).cwiseSqrt(); MatrixXd B = bXbPlusOne(K11, V2q); ArrayXd b = V2 * f + V1; ArrayXd c = (K11 * VectorXd(b)).array() * V2q; LLT<MatrixXd> chol(B); ArrayXd a = b - chol.solve(VectorXd(c)).array() * V2q; ArrayXd f0 = f; f = K11 * VectorXd(a); if ((f - f0).abs().maxCoeff() < 1e-5) { evidence = -(a * f).sum() / 2 + prob.log().sum() - log(chol.matrixL().determinant()); break; } } return -evidence; } public: static double get_evidence_optim(int n, const double *x, void *ex) { auto p = (BinaryHandwrittenDigit*)ex; return p->get_evidence(x); } }; int main() { auto sample = new BinaryHandwrittenDigit(); sample->read_data(); sample->make_matrix_L2(); double ninf = -INFINITY; double eps = 2.220446e-16; ArrayXd xini(2); xini << 2.85, 2.35; ArrayXd xout(2); double fmin = 0.0; int fail = 0; double abstol = ninf; double intol = sqrt(eps); void *ex = sample; double alpha = 1.0; double bet = 0.5; double gamm = 2.0; int trace = 0; int fncount = 0; int maxit = 500; auto start = chrono::system_clock::now(); nmmin(xini.size(), xini.data(), xout.data(), &fmin, BinaryHandwrittenDigit::get_evidence_optim, &fail, abstol, intol, ex, alpha, bet, gamm, trace, &fncount, maxit); auto end = chrono::system_clock::now(); long elapsed = chrono::duration_cast<chrono::milliseconds>(end - start).count(); printf("fmin=%f, fncount=%d, fail=%d\n", fmin, fncount, fail); for (int i = 0; i < xout.size(); i++) printf("%d, %f\n", i, xout[i]); printf("%d msec\n", elapsed); delete sample; }
- 投稿日:2019-05-06T18:19:13+09:00
【WPF】部分一致絞り込みが可能なComboBoxの作り方
項目数が多いComboBoxでは部分一致絞り込みが欲しくなる
WPFに限らずですが、あらかじめ決められた項目の中からユーザーに1つ選択させるためのコントロールとして、ComboBoxというものが標準で用意されています。「プルダウン」とも呼ばれたりすることがあるやるです。
しかし、選択肢が多くなればなるほど、多数の中から目的の項目を探し出す必要が生じるため、ただのComboBoxは使い勝手が悪くなります。そのようなときにあると便利なものが、入力した文字列と部分一致する項目のみに自動的に絞り込んでくれるComboBoxだったりします。
大まかな作成手順
ざっくり纏めると、下記の手順となります。
- ComboBoxを継承したコントロール(Xamlとコードビハインド)を作成する。
- 作成したコントロールをComboBoxと同じような感覚で使用する。
以上。
作成手順の詳細
例として、空のWPFプロジェクト(プロジェクト名はTestBenchWPFとする)に作成していきます。
1. 自作のコントロール用のフォルダを作成する。
プロジェクトを右クリック →「追加」→「新しいフォルダー」で作成します。
フォルダ名は「Controls」とします。
2. Controlsフォルダ内に、新規のユーザーコントロールを作成する。
Controlsフォルダを右クリック →「追加」→「ユーザーコントロール」で作成します。
名前は「PartialSearchComboBox」とします。
3. PartialSearchComboBox.xamlを修正する。
BEFORE<UserControl x:Class="TestBenchWPF.Controls.PartialSearchComboBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:TestBenchWPF.Controls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> </Grid> </UserControl>AFTER<ComboBox x:Class="TestBenchWPF.Controls.PartialSearchComboBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" IsEditable="True"> <!-- ↑任意の文字列を入力可能にするため、IsEditableをTrueにすることは必須。 --> </ComboBox>4. PartialSearchComboBox.xaml.csを修正する。
以下のように実装します。
PartialSearchComboBox.xaml.csusing System.Windows.Controls; using System.Windows.Controls.Primitives; namespace TestBenchWPF.Controls { /// <summary> /// PartialSearchComboBox.xaml の相互作用ロジック /// </summary> public partial class PartialSearchComboBox : ComboBox { private TextBox _textBox = null; private Popup _popUp = null; public PartialSearchComboBox() { InitializeComponent(); this.Loaded += delegate { _textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox; _popUp = this.Template.FindName("PART_Popup", this) as Popup; if (_textBox != null) { _textBox.TextChanged += delegate { if (!_popUp.IsOpen && string.IsNullOrEmpty(_textBox.Text)) { // プログラムの処理によってコンボボックス内のテキストが空に上書きされた場合、ここに入る。 // この処理がないと、コンボボックス内のテキストを空に上書きするときにプルダウンが開いてしまう。 this.Items.Filter += obj => { // プルダウン部分へ適用されているフィルターを解除する。 return true; }; return; } // 入力がある都度、即時フィルターをかける。 this.Items.Filter += obj => { if (!(obj is ComboBoxItem)) { return true; } var item = obj as ComboBoxItem; if (((string)item.Content).Contains(_textBox.Text)) { //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。 // フィルターを通過すると、展開された選択肢の中に表示される。 return true; } return false; }; _popUp.IsOpen = true; }; _textBox.GotFocus += delegate { if (!_popUp.IsOpen) { // コンボボックスの入力欄にフォーカスが当たったとき、プルダウンを展開する。 _popUp.IsOpen = true; } }; } }; } } }作成したComboBoxの使い方
上記手順で作成したPartialSearchComboBoxを実際に使ってみます。
簡単な例として、MainWindow.xamlの上に都道府県のコンボボックスとして設置してみます。BEFORE<Window x:Class="TestBenchWPF.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:TestBenchWPF" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> </Grid> </Window>AFTER<Window x:Class="TestBenchWPF.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:TestBenchWPF" xmlns:cc="clr-namespace:TestBenchWPF.Controls" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <cc:PartialSearchComboBox HorizontalAlignment="Center" VerticalAlignment="Center" Height="32" Width="120"> <ComboBoxItem Content="北海道"/> <ComboBoxItem Content="青森県"/> <!-- 中略 --> <ComboBoxItem Content="鹿児島県"/> <ComboBoxItem Content="沖縄県"/> </cc:PartialSearchComboBox> </Grid> </Window>動作のサンプル
これをビルド・実行してComboBox上に任意の文字列を入力すると、部分一致する都道府県のみに絞り込まれるようになることが分かります。
- 投稿日:2019-05-06T18:19:13+09:00
【WPF】部分一致インクリメンタルサーチが可能なComboBoxの作り方
項目数が多いComboBoxでは「部分一致検索」が欲しくなる
WPFに限らずですが、あらかじめ決められた項目の中からユーザーに1つ選択させるためのコントロールとして、ComboBoxというものが標準で用意されています。「プルダウン」とも呼ばれたりすることがあるやるです。
しかし、選択肢が多くなればなるほど、多数の中から目的の項目を探し出す必要が生じるため、ComboBoxは使い勝手が悪くなります。そのようなときにあると便利なものが、入力した文字列と部分一致する項目のみに自動的に絞り込んでくれるComboBoxだったりします。
本記事では、そのようなComboBoxの手軽な作り方をご紹介します。大まかな手順
ざっくり纏めると、下記の手順となります。
- ComboBoxを継承したコントロール(Xamlとコードビハインド)を作成する。
- 作成したコントロールをComboBoxと同じような感覚で使用する。
以上。
作成手順
例として、空のWPFプロジェクト(プロジェクト名はTestBenchWPFとする)に作成していきます。
1. 自作のコントロール用のフォルダを作成する。
プロジェクトを右クリック →「追加」→「新しいフォルダー」で作成します。
フォルダ名は「Controls」とします。
2. Controlsフォルダ内に、新規のユーザーコントロールを作成する。
Controlsフォルダを右クリック →「追加」→「ユーザーコントロール」で作成します。
名前は「PartialSearchComboBox」とします。
3. PartialSearchComboBox.xamlを修正する。
BEFORE<UserControl x:Class="TestBenchWPF.Controls.PartialSearchComboBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:TestBenchWPF.Controls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> </Grid> </UserControl>AFTER<ComboBox x:Class="TestBenchWPF.Controls.PartialSearchComboBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" IsEditable="True"> <!-- ↑任意の文字列を入力可能にするため、IsEditableをTrueにすることは必須。 --> </ComboBox>4. PartialSearchComboBox.xaml.csを修正する。
以下のように実装します。
PartialSearchComboBox.xaml.csusing System.Windows.Controls; using System.Windows.Controls.Primitives; namespace TestBenchWPF.Controls { /// <summary> /// PartialSearchComboBox.xaml の相互作用ロジック /// </summary> public partial class PartialSearchComboBox : ComboBox { private TextBox _textBox = null; private Popup _popUp = null; public PartialSearchComboBox() { InitializeComponent(); this.Loaded += delegate { _textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox; _popUp = this.Template.FindName("PART_Popup", this) as Popup; if (_textBox != null) { _textBox.TextChanged += delegate { if (!_popUp.IsOpen && string.IsNullOrEmpty(_textBox.Text)) { // プログラムの処理によってコンボボックス内のテキストが空に上書きされた場合、ここに入る。 // この処理がないと、コンボボックス内のテキストを空に上書きするときにプルダウンが開いてしまう。 this.Items.Filter += obj => { // プルダウン部分へ適用されているフィルターを解除する。 return true; }; return; } // 入力がある都度、即時フィルターをかける。 this.Items.Filter += obj => { if (!(obj is ComboBoxItem)) { return true; } var item = obj as ComboBoxItem; if (((string)item.Content).Contains(_textBox.Text)) { //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。 // フィルターを通過すると、展開された選択肢の中に表示される。 return true; } return false; }; _popUp.IsOpen = true; }; _textBox.GotFocus += delegate { if (!_popUp.IsOpen) { // コンボボックスの入力欄にフォーカスが当たったとき、プルダウンを展開する。 _popUp.IsOpen = true; } }; } }; } } }使い方
上記手順で作成したPartialSearchComboBoxを実際に使ってみます。
簡単な例として、MainWindow.xamlの上に都道府県のコンボボックスとして設置してみます。BEFORE<Window x:Class="TestBenchWPF.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:TestBenchWPF" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> </Grid> </Window>AFTER<Window x:Class="TestBenchWPF.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:TestBenchWPF" xmlns:cc="clr-namespace:TestBenchWPF.Controls" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <cc:PartialSearchComboBox HorizontalAlignment="Center" VerticalAlignment="Center" Height="32" Width="120"> <ComboBoxItem Content="北海道"/> <ComboBoxItem Content="青森県"/> <!-- 中略 --> <ComboBoxItem Content="鹿児島県"/> <ComboBoxItem Content="沖縄県"/> </cc:PartialSearchComboBox> </Grid> </Window>これを実行してプルダウン上に任意の文字列を入力すると、部分一致する都道府県のみに絞り込まれるようになります。
- 投稿日:2019-05-06T17:28:50+09:00
パフォーマンスを本気で気にするLINQプログラマのためのオペーレータ表
標準ライブラリのLINQオペレータにはパフォーマンス向上のための様々なトリックが仕込まれている。
実装をざっと調べたので、その成果をここにまとめておく。
- 調査対象は.Net Core 2.。
- オーダーが同じだからと言ってパフォーマンスが同じとは限らない。
- 結構雑な調査なので信用しすぎないように。
- 項目について
- シグニチャの特殊化 :オーバーロードによって最適化の度合いが異なる場合、各オーバーロードを表す。
- ソースの特殊化 : ソースシーケンスの具象型によって最適化の度合いが異なる場合、各具象型を表す。
- コールスタック : オペレータ利用による、
MoveNext
/Current
のコールスタック深さの増加分を表す。深くなるとcallvirt
呼び出しが増えるためパフォーマンスが劣化する。- 戻り値の特殊化型 : オペレータの戻り値が他のオペレータのパフォーマンス向上に寄与するような具象型を持つとき、その具象型を列挙する。
- オーダーの記号について
- $n$: ソースシーケンスの要素数。複数のソースからなる場合は添え字が省略されている。
- $k$: ソースの要素値とオペレータの引数で決まる定数。O記法ではふつうこのような値を使わないが、LINQを考える上ではなかなかに効いてくるので便宜的に導入する。
集計メソッド
メソッド名 シグニチャの特殊化 ソースの特殊化 時間計算量 備考 Aggregate
$O(n)$ Any
$O(n)$ All
$O(n)$ Average
$O(n)$ Contains
$O(n)$ Count
$O(n)$ ElementAt
$O(n)$ IPartition
$O(1)$ IList
$O(1)$ ElementAtOrDefault
$O(n)$ IPartition
$O(1)$ IList
$O(1)$ First
$O(1)$ IPartition
$O(1)$ IList
$O(1)$ FirstOrDefault
$O(1)$ IPartition
$O(1)$ IList
$O(1)$ Last
$O(n)$ IPartition
$O(1)$ IList
$O(1)$ LastOrDefault
$O(n)$ IPartition
$O(1)$ IList
$O(1)$ Max
$O(n)$ Min
$O(n)$ SequenceEqual
$O(n)$ 両シーケンスが ICollection
の場合、事前に要素数チェックが入る。
両シーケンスがIListの場合、イテレータではなくインデクサでアクセスするためcallvirt呼び出しが減る。ICollection
$O(n)$ IList
$O(n)$ Single
$O(1)$ シーケンスが IList
の場合、Count
とインデクサにより処理される。IList
$O(1)$ Sum
$O(n)$ ToArray
$O(n)$ シーケンスが IListProvider
の場合、各インターフェースに実装されたアルゴリズムで処理される。IListProvider
$O(n)$ ToList
$O(n)$ IListProvider
$O(n)$ ToDictionary
$O(n)$ シーケンスが配列または List
の場合、専用のアルゴリズムで処理される。
シーケンスがICollection
の場合、Count
をもとに初期キャパシティを取得する。ICollection
$O(n)$ T[]
$O(n)$ List
$O(n)$ ToHashSet
$O(n)$ 射影メソッド
メソッド名 シグニチャの特殊化 ソースの特殊化 コールスタック 時間計算量 戻り値の特殊化型 備考 最初の1件 全件 Append
1 $O(1)$ $O(n)$ AppendPrependIterator
,Iterator
,IListProvider
AppendPrependIterator
はソースと前後のリンクリストを保持する。AppendPrependIterator
へのAppend
/Prepend
はリンクリストを延長するだけなのでコールスタックが深くならない。AppendPrependIterator
0 $O(1)$ $O(n)$ Prepend
1 $O(1)$ $O(n)$ AppendPrependIterator
,Iterator
,IListProvider
AppendPrependIterator
0 $O(1)$ $O(n)$ OfType
1 $O(1)$ $O(n)$ Cast
1 $O(1)$ $O(n)$ Concat
1 $O(1)$ $\sum O(n)$ ConcatIterator
,Iterator
,IListProvider
ConcatIterator
はシーケンスのリンクを保持する。ConcatIterator
へのConcat
はリンクを延長するだけなのでコールスタックが深くならない。ConcatIterator
0 $O(1)$ $\sum O(n)$ DefaultIfEmpty
1 $O(1)$ $O(n)$ Distinct
1 $O(1)$ $O(n)$ Iterator
,IListProvider
Except
1 $O(n_{inner})$ $\sum O(n)$ Set
に依存。GroupJoin
1 $O(n_{inner})$ $\sum O(n)$ Lookup
に依存。GroupBy
1 $O(n)$ $O(n)$ Intersect
1 $O(n_{inner})$ $\sum O(n)$ Set
に依存。Join
1 $O(n_{inner})$ $\sum O(n)$ Lookup
に依存。ToLookup
1 $O(n)$ $O(n)$ IListProvider
戻り値の実体は Lookup
。OrderBy
1 $O(n)$ $O(n)$ IPartition
,IOrderedEnumerable
Reverse
1 $O(n)$ $O(n)$ Select
Func<T, TResult>
1 $O(1)$ $O(n)$ Iterator
,IListProvider
配列、 List
はIList
と同じオーダーだが、一部のメソッド呼び出しにより強力な最適化が働く。
セレクタにインデックスをとるオーバーロードは最適化が弱い。Iterator
1 $O(1)$ $O(n)$ Iterator
,IListProvider
T[]
1 $O(1)$ $O(n)$ Iterator
,IPartition
List
1 $O(1)$ $O(n)$ Iterator
,IPartition
IList
1 $O(1)$ $O(n)$ Iterator
,IPartition
IPartition
1 $O(1)$ $O(n)$ Iterator
,IPartition
Func<T, int, TResult>
1 $O(1)$ $O(n)$ SelectMany
Func<TSource, IEnumerable<TResult>>
1 $O(1)$ $O(n)$ Iterator
,IListProvider
セレクタにインデックスをとるオーバーロードは最適化が弱い。 Func<TSource, int, IEnumerable<TResult>>
1 $O(1)$ $O(n)$ Skip
1 $O(k)$ $O(n)$ Iterator
IIterator
0 ~ 1 $O(k)$ $O(n)$ Iterator
IPartition
0 $O(1)$ $O(n)$ IPartition
SkipWhile
1 $O(k)$ $O(n)$ SkipLast
1 $O(1)$ $O(n)$ Take
1 $O(1)$ $O(k)$ Iterator
,IPartition
IPartition
0 $O(1)$ $O(k)$ Iterator
,IPartition
IList
1 $O(1)$ $O(k)$ Iterator
,IPartition
TakeWhile
1 $O(1)$ $O(k)$ TakeLast
1 $O(n)$ $O(n)$ IPartition
0 $O(1)$ $O(k)$ Iterator
,IPartition
IList
1 $O(1)$ $O(k)$ Iterator
,IPartition
Union
1 $O(1)$ $\sum O(n)$ UnionIterator
,Iterator
,IListProvider
UnionIterator
はシーケンスのリンクを保持し、イテレート時に重複の除外を行う。ソースがUnionIterator
で、かつ同じcomparer
を使っているときはリンクを追加するだけなのでコールスタックが深くならない。UnionIterator
0 ~ 1 $O(1)$ $\sum O(n)$ UnionIterator
,Iterator
,IListProvider
Where
1 $O(k)$ $O(n)$ Iterator
,IListProvider
配列、 List
は一部のメソッド呼び出しにより強力な最適化が働く。
セレクタにインデックスをとるオーバーロードは最適化が弱い。T[]
1 $O(k)$ $O(n)$ Iterator
,IListProvider
List
1 $O(k)$ $O(n)$ Iterator
,IListProvider
Func<T, int, bool>
1 $O(k)$ $O(n)$ Iterator
,IListProvider
Zip
1 $O(1)$ $\min(O(n))$ 最適化に用いられる型
Iterator
クラスLINQオペレータで広く利用される抽象クラス。
IEnumerable
,IEnumerator
を実装している。
ステートとスレッドIDを管理しており、自身を生成したスレッド内かつ唯一の利用ならGetEnumerator
が自身を返すことでオブジェクト数を増やさないようになっていたりする。
ステートフィールドの解釈は子クラス側に委ねられており、OOP的にはあまりよろしくない設計となっているが、パフォーマンスを出すためには仕方ないことなのだろうか。
IListProvider
インターフェース
ToArray
,ToList
の専用実装を用意できるようにするインターフェース。
IPartition
インターフェースランダムアクセス可能なシーケンスであることを表すインターフェース。参照型向けの
Span
みたいなもの。
Lookup
クラス
ILookup
の実装。ToLookup
やGroupBy
などで用いられる、グループキーとシーケンスの軽量な辞書。
Set
クラス軽量なハッシュセットクラス。
System.Collections.Generic.HashSet
はコアライブラリで使うにはリッチすぎるということだろうか。まとめ
- 同じオペレータを連続適用すると効率が良い(ことがそこそこある)
Select
/Where
はインデックス込みで評価できるオーバーロードを使うと最適化が利かなくなったりする。最後に
正直こういうの考える必要があるほどパフォーマンスが大事なら素直に
foreach
で書いた方がいいと思う。
- 投稿日:2019-05-06T10:23:36+09:00
C# MicroBatchFramework バッチクラスの定義に関する検証
MicroBatchFramework とは
【GitHub】MicroBatchFramework より引用
MicroBatchFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multiple contained batch program. Easy to bind argument to the simple method definition. It built on .NET Generic Host so you can configure Configuration, Logging, DI, etc can load by the standard way.
コンソールアプリケーション向けのフレームワークです。こう言ってしまうと省略しすぎですが、コマンドラインで指定されたメソッドやパラメーターを簡単な手順でバインドさせることができます。.NET Core の機能拡張に伴い、マイクロサービスなどコンソールアプリケーションの用途が増えていくことが予想される中で、このようなインフラストラクチャの拡充には非常に注目しています。
検証内容
手始めとして、バッチクラスの定義に関する検証を行いました。
- 別アセンブリに定義したバッチクラスのメソッドを起動できるか
- Command 属性を付与しないメソッドが存在してもよいか
MicroBatchFramework
のバージョンは 1.2.0 です。検証に使用したクラス
定義するアセンブリと名前空間が異なる次の4つのバッチクラスを定義しました。
クラス 名前空間 説明 SampleBatch1 NetCoreApp1 起動アセンブリのルート名前空間に定義したバッチクラス SampleBatch2 NetCoreApp1.Batches 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス SampleBatch3 NetCoreApp1 起動アセンブリのルート名前空間外に定義したバッチクラス SampleBatch4 BatchLibrary1 別アセンブリに定義したバッチクラス メソッドの処理内容はすべて同じであるため割愛しています。コンソールにログを出力するだけです。
// 起動アセンブリのルート名前空間に定義したバッチクラス namespace NetCoreApp1 { class SampleBatch1 : BatchBase { [Command("exec1")] public void Execute(string name1, int repeat1 = 3) { for (int i = 0; i < repeat1; i++) { this.Context.Logger.LogInformation($"{this.GetType().Name}.Execute from {name1} ({i + 1}/{repeat})"); } } } } // 起動アセンブリのルート名前空間の配下に定義したバッチクラス namespace NetCoreApp1.Batches { class SampleBatch2 : BatchBase { [Command("exec2")] public void Execute(string name2, int repeat2 = 3) {} } } // 起動アセンブリのルート名前空間外に定義したバッチクラス namespace Batches { class SampleBatch3 : BatchBase { [Command("exec3")] public void Execute(string name3, int repeat3 = 3) {} } } // 別アセンブリに定義したバッチクラス namespace BatchLibrary1 { public class SampleBatch4 : BatchBase { [Command("exec4")] public void Execute(string name4, int repeat4 = 3) {} } }検証に使用したバッチファイル
list, help コマンドと、バッチクラスに定義したメソッドを実行します。
@echo ===== list ===== @dotnet NetCoreApp1.dll list @echo ===== help ===== @dotnet NetCoreApp1.dll help @echo ===== exec1 ===== @dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3 @echo ===== SampleBatch1.Execute ===== @dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3 @echo ===== exec2 ===== @dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3 @echo ===== SampleBatch2.Execute ===== @dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3 @echo ===== exec3 ===== @dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3 @echo ===== SampleBatch3.Execute ===== @dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3 @echo ===== exec4 ===== @dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3 @echo ===== SampleBatch4.Execute ===== @dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3 @dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3
ホストの起動方法
バッチクラスを指定せずに起動
RunBatchEngineAsync メソッドで起動します。
static async Task Main(string[] args) { await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args); }バッチクラスの型を指定して起動
RunBatchEngineAsync<T> メソッドで起動します。
static async Task Main(string[] args) { // 起動アセンブリのルート名前空間の配下に定義したバッチクラス await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<SampleBatch1>(args); } static async Task Main(string[] args) { // 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<Batches.SampleBatch2>(args); } static async Task Main(string[] args) { // 起動アセンブリのルート名前空間外に定義したバッチクラス await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<global::Batches.SampleBatch3>(args); } static async Task Main(string[] args) { // 別アセンブリに定義したバッチクラス await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<BatchLibrary1.SampleBatch4>(args); }実行結果
list コマンド
- 起動アセンブリに定義されているバッチクラスのメソッドが列挙されます。
- 起動アセンブリ以外のアセンブリに定義されているバッチクラスを指定して起動した場合、そのアセンブリに定義されているバッチクラスも列挙対象に追加されるようです。
実行コマンド@echo ===== list ===== @dotnet NetCoreApp1.dll list
バッチクラスを指定せずに起動===== list ===== list of methods: SampleBatch3.Execute SampleBatch1.Execute SampleBatch1.ExecuteNotCommand SampleBatch2.Execute
起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動===== list ===== list of methods: SampleBatch3.Execute SampleBatch1.Execute SampleBatch1.ExecuteNotCommand SampleBatch2.Execute
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動===== list ===== list of methods: SampleBatch3.Execute SampleBatch1.Execute SampleBatch1.ExecuteNotCommand SampleBatch2.Execute
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動===== list ===== list of methods: SampleBatch3.Execute SampleBatch1.Execute SampleBatch1.ExecuteNotCommand SampleBatch2.Execute
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動===== list ===== list of methods: SampleBatch3.Execute SampleBatch1.Execute SampleBatch1.ExecuteNotCommand SampleBatch2.Execute SampleBatch4.Execute
helpコマンド
- RunBatchEngineAsync メソッドでホストを起動した場合
- エラーが発生します。
- help の後にメソッド名を指定すれば、そのメソッドのヘルプを表示することができます。メソッドに対して付与した Command 属性のコマンド名を指定した場合はエラーになります。
- RunBatchEngineAsync<T> メソッドでホストを起動した場合
- T に指定したバッチクラスのメソッドのヘルプが列挙されます。
@echo ===== help ===== @dotnet NetCoreApp1.dll helpバッチクラスを指定せずに起動===== help ===== Type or method does not found on this Program. args: help
バッチクラスを指定せずに起動(helpの後にメソッド名を指定)> dotnet NetCoreApp1.dll help SampleBatch1.Execute exec1: -name1: String -repeat1: [default=3]Int32起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動===== help ===== exec1: -name1: String -repeat1: [default=3]Int32
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動===== help ===== exec2: -name2: String -repeat2: [default=3]Int32
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動===== help ===== exec3: -name3: String -repeat3: [default=3]Int32
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動===== help ===== exec4: -name4: String -repeat4: [default=3]Int32
メソッド実行
- RunBatchEngineAsync メソッドでホストを起動した場合
- 起動アセンブリに定義されているバッチクラスのメソッドを実行できます。参照アセンブリに定義されているバッチクラスは実行できません。
- コマンドラインで指定するメソッド名には、そのメソッドのクラス名と型名({クラス名}.{メソッド名})を指定します。
- RunBatchEngineAsync<T> メソッドでホストを起動した場合
- T で指定したバッチクラスのメソッドを実行できます。
- コマンドラインで指定するメソッド名には、そのメソッドに対して Command 属性で設定したコマンド名を指定します。
実行コマンド@echo ===== exec1 ===== @dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3 @echo ===== SampleBatch1.Execute ===== @dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3 @echo ===== exec2 ===== @dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3 @echo ===== SampleBatch2.Execute ===== @dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3 @echo ===== exec3 ===== @dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3 @echo ===== SampleBatch3.Execute ===== @dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3 @echo ===== exec4 ===== @dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3 @echo ===== SampleBatch4.Execute ===== @dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3 @dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3
バッチクラスを指定せずに起動===== exec1 ===== Type or method does not found on this Program. args: exec1 -name1 test -Repeat1 3 ===== SampleBatch1.Execute ===== SampleBatch1.Execute from test (1/3) SampleBatch1.Execute from test (2/3) SampleBatch1.Execute from test (3/3) ===== exec2 ===== Type or method does not found on this Program. args: exec2 -name2 test -Repeat2 3 ===== SampleBatch2.Execute ===== SampleBatch2.Execute from test (1/3) SampleBatch2.Execute from test (2/3) SampleBatch2.Execute from test (3/3) ===== exec3 ===== Type or method does not found on this Program. args: exec3 -name3 test -Repeat3 3 ===== SampleBatch3.Execute ===== SampleBatch3.Execute from test (1/3) SampleBatch3.Execute from test (2/3) SampleBatch3.Execute from test (3/3) ===== exec4 ===== Type or method does not found on this Program. args: exec4 -name4 test -Repeat4 3 ===== SampleBatch4.Execute ===== Type or method does not found on this Program. args: SampleBatch4.Execute -name4 test -Repeat4 3 Type or method does not found on this Program. args: BatchLibrary1.SampleBatch4.Execute -name4 test -Repeat4 3
起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動===== exec1 ===== SampleBatch1.Execute from test (1/3) SampleBatch1.Execute from test (2/3) SampleBatch1.Execute from test (3/3) ===== SampleBatch1.Execute ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== exec2 ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== SampleBatch2.Execute ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== exec3 ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== SampleBatch3.Execute ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== exec4 ===== exec1: -name1: String -repeat1: [default=3]Int32 ===== SampleBatch4.Execute ===== exec1: -name1: String -repeat1: [default=3]Int32 exec1: -name1: String -repeat1: [default=3]Int32
起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動===== exec1 ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== SampleBatch1.Execute ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== exec2 ===== SampleBatch2.Execute from test (1/3) SampleBatch2.Execute from test (2/3) SampleBatch2.Execute from test (3/3) ===== SampleBatch2.Execute ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== exec3 ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== SampleBatch3.Execute ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== exec4 ===== exec2: -name2: String -repeat2: [default=3]Int32 ===== SampleBatch4.Execute ===== exec2: -name2: String -repeat2: [default=3]Int32 exec2: -name2: String -repeat2: [default=3]Int32
起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動===== exec1 ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== SampleBatch1.Execute ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== exec2 ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== SampleBatch2.Execute ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== exec3 ===== SampleBatch3.Execute from test (1/3) SampleBatch3.Execute from test (2/3) SampleBatch3.Execute from test (3/3) ===== SampleBatch3.Execute ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== exec4 ===== exec3: -name3: String -repeat3: [default=3]Int32 ===== SampleBatch4.Execute ===== exec3: -name3: String -repeat3: [default=3]Int32 exec3: -name3: String -repeat3: [default=3]Int32
別アセンブリに定義したバッチクラスSampleBatch4を指定して起動===== exec1 ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== SampleBatch1.Execute ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== exec2 ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== SampleBatch2.Execute ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== exec3 ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== SampleBatch3.Execute ===== exec4: -name4: String -repeat4: [default=3]Int32 ===== exec4 ===== SampleBatch4.Execute from test (1/3) SampleBatch4.Execute from test (2/3) SampleBatch4.Execute from test (3/3) ===== SampleBatch4.Execute ===== exec4: -name4: String -repeat4: [default=3]Int32 exec4: -name4: String -repeat4: [default=3]Int32
Command 属性を付与しないメソッド
Command 属性はメソッドに対してコマンド名や説明を設定するための属性ですが、この属性が付与されていないメソッドが存在した場合、若干挙動が変わります。
namespace NetCoreApp1 { class SampleBatch1 : BatchBase { [Command("exec1")] public void Execute(string name1, int repeat1 = 3) {} // Command属性を付与しない public void ExecuteNotCommand(string name, int repeat = 3) {} } }list コマンド
- Command 属性が付与されていないメソッドも列挙対象に含まれます。
help メソッド
- RunBatchEngineAsync メソッドでホストを起動した場合
- Command 属性が付与されていないメソッドも、help コマンドの後にメソッド名({クラス名}.{メソッド名})を指定すれば表示されます。
- RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
- そのメソッドのヘルプも表示されますが、コマンド名には "argument list" と表示されます。コマンド名には空の string 配列が設定されているのではないかと予想されます。
- RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
- そのメソッドのヘルプは表示されません。
メソッド実行
- RunBatchEngineAsync メソッドでホストを起動した場合
- Command 属性が付与されていないメソッドも実行できます。
- RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
- そのメソッドも、メソッド名({クラス名}.{メソッド名})を指定すれば実行できます。
- T で指定された型以外のバッチクラスのメソッドを呼び出したときにコンソールに表示される内容が変わります。「コマンド名が見つからない」というエラーが表示されます。
Required parameter "name" not found in argument. args: exec2 -name2 test -Repeat1 3
- RunBatchEngineAsync<T> メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
- そのメソッドは実行できません。
まとめ
一つのアプリケーションに複数のメソッドを実装する場合、一つのバッチクラスに集約するか複数のバッチクラスに分割するかによってホストの起動方法が変わります。コマンドラインの指定方法も変わります。
- 一つのバッチクラスに集約
- RunBatchEngineAsync<T> メソッドでホストを起動すれば、Command 属性で設定したコマンド名で指定するこ とができます。
- 実行対象のバッチクラスは起動アセンブリから参照できる型であればよく、別アセンブリに定義されていても構いません。
- 複数のバッチクラスに分割
- RunBatchEngineAsync<T> メソッドでは単一のバッチクラスのメソッドしか実行できないため、RunBatchEngineAsync メソッドでホストを起動することになります。
- Command 属性で設定したコマンド名ではなく、メソッド名({クラス名}.{メソッド名})指定することになります。
- 実行対象のバッチクラスは起動アセンブリに定義する必要があります。
Command 属性はコマンド名や説明を設定できますし、付与することに対するデメリットはなさそうです。必ず付与するルールにしてもよさそうです。
- 投稿日:2019-05-06T09:59:29+09:00
C# x SendGridでメールを送信する 1
今日はSendGridを使ってメールを送ってみます。問い合わせフォームのメールからMA的なメール送信まで幅広く利用することを想定しています。利用用途は幅広く提供されている機能も多いので何度かに分けて整理したいと思います。 マイクロソフトからもドキュメントがまとめられているので、こちらのドキュメントも参照してください。https://docs.microsoft.com/en-us/azure/sendgrid-dotnet-how-to-send-email
環境
Visual Studio 2019, C#, ASP.NET Core 2.2
SendGrid API Version 3
もうすでにASP.NET Core プロジェクトがあるものとします。SendGridとは?
SendGridはメール配信サービスです。名だたるサービスがSendGridを使ってメール配信をおこなっています。自社でメールサーバーを持つような時代は終わって、SendGridに面倒なメンテ作業を任せて、メールを送ることだけにフォーカスすることができます。
Send Grid Official Page
https://sendgrid.com/Azureからもマーケットプレイスで購入することができます。Azure Marketplaceから購入すると月25,000通まで無料。
Azure Market Placeから購入
Azureのアカウントを持っている方はこちらのリンクからSendGridのサービスを購入。https://portal.azure.com/#create/Sendgrid.sendgrid
赤いマークがついてるフィールは必須なので埋めてください。
今日は一番右にあるフリープランで進めます
ライブラリーの準備
SendGridを使ってメールを送信するには便利なライブラリーがあるので下記のNugetコマンドから入手します。
SendGrid、C#用SDKGitHubはこちらから
https://github.com/sendgrid/sendgrid-csharp
.Net Core要のサンプルコードはこちらから
https://github.com/sendgrid/sendgrid-csharp/tree/master/ExampleCoreProjectパッケージマネージャーコンソールより
PM> Install-Package SendGridSendGrid Version 9.11.0 がインストールされています。
APIの準備
作成したSendGridメニューにある
Manage
をクリックします。
SendGridの管理画面がたちがりますので、右側にある
Setting
からAPI Keys
をクリックします。右上にある
Create API Key
をクリック。APIアクセスのアクセスレベルを選択して、名前を入力して作成します。ここで生成されたAPI KeyをAppSettings.jsonに入力します。
これで一旦設定はおしまいです。次は実際にサービスを書いてみます。
サービスを書く
まず、メールを送るサービスである
サービスクラスとインターフェイスを書きます。他のサービスクラスも格納するため、サービスフォルダーとインターフェイスフォルダーを作成し。 サービスフォルダーに EmailSernder.csというクラス、インターフェイスフォルダーの中にIEmailSender.cs というEmailSender のインターフェイスを作成します。
IEmailSender.csusing SendGrid.Helpers.Mail; using System.Threading.Tasks; namespace wppSample.Services.Interfaces { public interface IEmailSender { Task SendEmailAsyc(SendGridMessage msg); } }EmailSender.csusing Microsoft.Extensions.Options; using SendGrid; using SendGrid.Helpers.Mail; using System.Threading.Tasks; using wppSample.Models; using wppSample.Services.Interfaces; namespace wppSample.Services { public class EmailSender : IEmailSender { /// <summary> /// For Secure API Value /// </summary> private readonly IOptions<AppSettings> _config; /// <summary> /// Construct and get _config /// </summary> /// <param name="config"></param> public EmailSender(IOptions<AppSettings> config) { _config = config; } /// <summary> /// Send Email via SendGrid /// </summary> /// <param name="msg"></param> /// <returns></returns> public async Task SendEmailAsyc(SendGridMessage msg) { var client = new SendGridClient(_config.Value.SendGrid.ApiKey); var response = await client.SendEmailAsync(msg); } } }これでサービスをDIすることによって、メールが送れる状態になっています。次は具体的にメールフォームを送信したときの処理を書いてみます。
問い合わせフォームからメールを送る
まずは問い合わせフォームのRazorページ
ContactUs.cshtml@page @model wppSample.Pages.ContactUsModel @{ ViewData["Title"] = "お問合せフォーム"; } <div class="container py-4"> <h1>お問合せ</h1> <p> 下記のお問合せフォームをご利用ください。1-2営業日で返信させていただいています。 </p> <div class="py-3"> @using (Html.BeginForm(FormMethod.Post)) { <div class="form-group"> @Html.Label("お名前") @Html.TextBox("name", "", new { @class = "form-control border-color-2", @type = "Text", @style = "background-color:#FFFFEF", @placeholder = "お名前" }) </div> <div class="form-group"> @Html.Label("email", "メールアドレス", new { @class = "color-2" }) @Html.TextBox("email", "", new { @class = "form-control border-color-2", @type = "Email", @style = "background-color:#FFFFEF", @placeholder = "メールアドレス" }) </div> <div class="form-group"> @Html.Label("inquiry", "問合せ内容", new { @class = "color-2" }) @Html.TextArea("inquiry", "", new { @class = "form-control border-color-2", @style = "background-color:#FFFFEF; height:200px;", @placeholder = "問合せ内容" }) </div> <div class="form-group"> <input type="submit" class="btn btn-primary btn-rounded-0" value="送信" /> </div> } </div> </div>OnPostの処理
ContactUs.cshtml.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using SendGrid.Helpers.Mail; using wppSample.Services.Interfaces; namespace wppSample.Pages { public class ContactUsModel : PageModel { private readonly IEmailSender _emailSender; /// <summary> /// Construct ContactUsModel /// </summary> /// <param name="emailSender"></param> public ContactUsModel(IEmailSender emailSender) { _emailSender = emailSender; } /// <summary> /// On Get Action /// </summary> public void OnGet() { } /// <summary> /// On Post Action /// </summary> /// <param name="name"></param> /// <param name="email"></param> /// <param name="inquiry"></param> public async Task<IActionResult> OnPostAsync(string name, string email, string inquiry) { // Send email to the customers to confirm the email sent var from = new EmailAddress() { Name = name, Email = email }; var to = new EmailAddress() { Name = "Support", Email = "support email address" }; var subject = "お問合せがありました"; var plainCointent = name + "さんより、問合せ内容: " + inquiry; var htmlContent = name + "さんの問合せ内容<br/><br/>" + inquiry; var msg = MailHelper.CreateSingleEmail(from, to,subject,plainCointent,htmlContent); // Send email to the customer support so that they can help the customer await _emailSender.SendEmailAsyc(msg); ViewData["Name"] = name; ViewData["Email"] = email; ViewData["Inquiry"] = inquiry; return RedirectToPage("Index"); } } }こんな感じの問合せページからメールが送れている事が確認できました。
次
次回はフォームのバリデーションや、本人へも問合せ内容を知らせるメールを送ったりなどリッチな問合せフォーム体験を書きたいと思います。今日は一旦SendGridを動かすところまでに。