メタな感じのプログラミング(プロ生 + わんくま 071118)
TRANSCRIPT
Dawn Huczek
メタな感じのプログラミング
自己紹介
・石川達也
・(株)Codeer 代表取締役
・Microsoft MVP
・ささいなことですが(ブログ)
・OSS
FriendlySelenium拡張
LambdicSql
Visual Studio and Development Technologies
http://ishikawa-tatsuya.hatenablog.com/
https://www.nuget.org/profiles/ishikawa-tatsuya
趣味はギターとライブラリ作成
Codeer Ltd.
こんなメンバーでやってます!
ソフトウェア開発でお悩みの方は、いつでもご相談ください
イントロダクション
そもそも、メタプログラミングとは
データ→プログラムプログラム→データプログラム→プログラム
このスライドにまとまってました。
https://www.slideshare.net/kmizushima/ss-6031153
つまり、身近なものとして使っている
・コンパイラ・コード生成ウィザード・デザイナ・インテリセンス
IDE系に多い
今日話すリフレクションツール
・DynamicObject
・リフレクション
・CodeDom
・Roslyn
・Expression
実例と一緒に話します
DynamicObject+
リフレクション
Friendly
別プロセスの内部APIを呼び出せるライブラリ
Friendlyのデモ
DynamicObject +リフレクション
テストコードの方ではC#のコードをデータとして取り込んでいる。そして、それを対象プロジェクト側に送ってリフレクションで実行させている
//var form = Application.OpenForms[0] var form = app.Type<Application>().OpenForms[0];form.BackColor = Color.Green;
var type = FindType("System.Windows.Forms.Application");var propOpenForms = type.GetProperty("OpenForms", flgs);var openForms = propOpenForms.GetValue(null);var method = openForms.GetType().GetMethod("get_Item", flgs);var form = method.Invoke(openForms, new object[] { 0 });var propBackColor = form.GetType().GetProperty("BackColor");propBackColor.SetValue(form, Color.Green);
dynamic
ダックタイプができるようになる機能
でも、それだけではない!
public class X{
public void Func() { }}
public class Y{
public void Func() { }}
public class Test{
public void Main(){
DuckType(new X());DuckType(new Y());
}
public void DuckType(dynamic target)=> target.Func();
}
DynamicOjbect
立派なメタプロツールDynamicObjectを継承したクラスをdynamicにするとC#の構文を動的に利用可能なデータに変換してくれる
public class DynamicObject {
//キャストpublic virtual bool TryConvert(ConvertBinder binder, out object result);//インデクサ (object this[int index])public virtual bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result);public virtual bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value);//プロパティ、フィールドpublic virtual bool TryGetMember(GetMemberBinder binder, out object result);public virtual bool TrySetMember(SetMemberBinder binder, object value);//メソッドpublic virtual bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result);//delegatepublic virtual bool TryInvoke(InvokeBinder binder, object[] args, out object result);
//いくつか端折ります…
}
使うものだけオーバーライドしたらいいよ
DynamicOjbect
var application = app.Type<Application>();
var openForms = application.OpenForms;
//class DynamicAppType : DynamicObjectpublic DynamicAppType(WindowsAppFriend app, string typeFullName)
=> _typeFullName = typeFullName;
dynamic Type<T>(this app) =>new DynamicAppType(app, typeof(T).FullName);
public override bool TryGetMember(GetMemberBinder binder, out object result)
{//プロパティー名がわかる "OpenForms"var propOrFieldName = binder.Name;
//タイプ名称とメンバ名称を送る//その情報があれば、相手プロセスでリフレクションを実行可能result = SendGetProperty(_typeFullName, propOrFieldName);return true;
}
public class DynamicAppType : DynamicObject
public class DynamicAppVar : DynamicObject
DynamicOjbect
var application = app.Type<Application>();
var openForms = application.OpenForms;
Friendlyでは二種類実装してます
型に対するstaticな操作
オブジェクトに対する操作
DynamicOjbect
DynamicAccessorhttps://github.com/neuecc/ChainingAssertion/blob/master/ChainingAssertion/ChainingAssertion.MSTest.cs
Friendlyのはちょっと一般的でないのでサンプルコードとしてはこちらが分かりやすいと思います。
privateな操作を可能にする実装
dynamic objX = obj.AsDynamic();var value = objX.Value;
何はともあれ、リフレクション
型情報を取り出す機能。C#でのメタプロの基本といっても過言ではない。型情報はプログラムからはメタデータと呼ばれる。文字列から目的のデータを取得できるので動的な処理が可能となる。
一番なじみ深いメタプロツール
・ Assembly・ Type・ MethodInfo・ PropertyInfo・ FieldInfo
【コラム】 タイプの探し方
//現在実行中のアセンブリまたは//Mscorlib.dll 内にある場合でないと無理var type = Type.GetType("MyLib.MyClass");
//お、おう・・・ AssemblyQualifiedNamevar type = Type.GetType(
"MyLib.MyClass, FullDotNetDll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
//現実的にはこんなところ//フルネームが同じのが二つあると正しく取れないけど//まあ、それは良いでしょう。var type = AppDomain.CurrentDomain.GetAssemblies().
Select(x => x.GetType(“MyLib.MyClass”)).Where(x => x != null).FirstOrDefault();
//Genericはちょっと面倒//A<B>var a = AppDomain.CurrentDomain.GetAssemblies().
Select(x => x.GetType("MetaTest.A`1")).Where(x => x != null).FirstOrDefault();
var b = AppDomain.CurrentDomain.GetAssemblies().Select(x => x.GetType("MetaTest.B")).Where(x => x != null).FirstOrDefault();
var generic = a.MakeGenericType(new[] { b });
【コラム】 タイプの探し方
【コラム】メソッドの探し方
//普通はこれでいいんだけど・・・
var binding = BindingFlags.Public | BindingFlags.NonPublic |BindingFlags.Static | BindingFlags.Instance;
var method = type.GetMethod("Func", binding, null, new [] { typeof(int) }, null);
//こういうの実装したいときstatic object Execute(Type type, object target, string func, params object[] args);
class Q { }class QQ: Q { }
class WWW{
public void Func(Q q) { }public void Func(string s) { }
}
Execute(typeof(WWW), new WWW(), "Func", new QQ());
public MethodInfo GetMethod(string name, BindingFlags bindingAttr,Binder binder, Type[] types, ParameterModifier[] modifiers);
static object Execute(Type type, object target, string func, params object[] args){
var binding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
MethodInfo matchMethod = null;var maybeMethods = new List<MethodInfo>();while (type != null && matchMethod == null){
foreach (var x in type.GetMethods(binding)){
switch (CheckMatch(func, args, x)){
case MethodMatch.Match:matchMethod = x;break;
case MethodMatch.Maybe:maybeMethods.Add(x);break;
default:break;
}}type = type.BaseType
}
//完全一致if (matchMethod != null) return matchMethod.Invoke(target, args);
//一つに絞れてることreturn maybeMethods.Single().Invoke(target, args);
}
オーバーロードの解決
【コラム】メソッドの探し方
static MethodMatch CheckMatch(string func, object[] args, MethodInfo methodInfo){
//名前や引数の数が違うと不一致if (methodInfo.Name != func) return MethodMatch.Diff;var parameters = methodInfo.GetParameters();if (parameters.Length != args.Length) return MethodMatch.Diff;
var methodMatch = MethodMatch.Match;for (int i = 0; i < args.Length && methodMatch != MethodMatch.Diff; i++){
var paramType = parameters[i].ParameterType;
//nullは値型でなければ一致してるかもしれないif (args[i] == null){
methodMatch = MethodMatch.Maybe;if (paramType.IsValueType) methodMatch = MethodMatch.Diff;
}//一致else if (paramType == args[i].GetType()) { }//代入できるなら一致してるかもしれないelse if (paramType.IsAssignableFrom(args[i].GetType())) methodMatch = MethodMatch.Maybe;//不一致else methodMatch = MethodMatch.Diff;
}
return methodMatch;}
【コラム】メソッドの探し方
StandardではType以下の情報取得が面倒・・・
TypeInfo typeInfo = type.GetTypeInfo();
TypeInfoの方に情報があって、わざわざGetTypeInfo()って呼ばないとダメだった。なんでこんな改悪したんだよ・・・・
1.2のコード
StandardではType以下の情報取得が面倒・・・結構改善!
てか、Typeの方に戻ってきた。やっぱ評判悪かったんじゃんw
2.0
まず、知らないDLLをロードできない→deps.json
全タイプ探すことができない→現在読み込まれているアセンブリの一覧が取れない
どうすんだこれ・・・
DotNetCoreではやりづらくなった
なんだか知らんけどこれもTypeInfo同様改善されることを望む・・・
暗黒な使い方だけでなく普通のリフレクション的な簡単な使い方もある。(怖くない
アプリのライフサイクル中に再コンパイルされる可能性のあるアセンブリを読み込むときはこっちを使おう。
こっちにもちょっと書いたよhttps://www.slideshare.net/tatsuyaishikawa7334/dot-netconf2017-vs
Mono.Cecil
//アセンブリ情報取得var asm = AssemblyDefinition.ReadAssembly(assemblyPath);
//タイプvar type = asm.Modules.SelectMany(e => e.Types).
Where(e => e.FullName == typeFullName).FirstOrDefault();
C#スクリプト
Quick shot
関数単体で実行するVS拡張(無料)
Quick shot デモ
https://marketplace.visualstudio.com/items?itemName=ishikawa-tatsuya.Quickshot
先日話したVS拡張の話はこちら
https://www.slideshare.net/tatsuyaishikawa7334/dot-netconf2017-vs
VS拡張はメタプロの宝庫
・コード解析・アセンブリ解析・コンパイル・コード生成・Etc
まあIDEなので当たり前
設定をC#で書く
DBへの接続設定をどするかで迷った
まずプロバイダ選ばないと・・・
でも、再配布できないものもあるし・・・
Nugetで取ったの選ばせるの?
そんなUI嫌すぎる!
どうすりゃいいねん・・・
コードで表現した設定してもらう方が分かりやすいよね!
コネクション取得のプロパティを書いておくと、DBへの接続が必要な時にはそれを使います。
特定のコンテキストではGUIでの設定より、XMLでの設定より、C#で設定する方が分かりやすい!
そして、C#のスクリプトをコンパイル手段があればそれが選択肢に入る!
昔から複雑なXMLの設定見るたびにこれならコードで書かせてくれよって思ってました。
C#で設定させるのも選択肢の一つ
Code Dom
C#スクリプト利用するための元祖大昔から存在する新しい構文は使えない標準で使える実はかなり暗黒なことも可能Roslynより高速
var code = @"public class Abc{
public int GetValue()=>100;};";
var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v3.5" } });
var param = new CompilerParameters { GenerateInMemory = true };var compilerResults = codeProvider.CompileAssemblyFromSource(param, code);
//アセンブリからリフレクションで必要な型を取り出すvar asm = compilerResults.CompiledAssembly;
Roslyn
コンパイラプラットフォームコード解析からコンパイルまで最新の構文でも対応している
多機能だけど、それぞれ使いやすい
var code = @"return 1 + 2 + 3;";
var script = CSharpScript.Create<int>(code);var scriptReult = script.RunAsync();scriptReult.Wait();var val = scriptReult.Result.ReturnValue;
共通の型を定義しておくと使いやすい
var code = @"public class Abc : MetaTest.ITest{
public int GetValue()=>100;}return new Abc();";
//インターフェイスの定義されているアセンブリを参照var option = ScriptOptions.Default.AddReferences(GetType().Assembly);//スクリプト実行var script = CSharpScript.Create<ITest>(code, option);var scriptReult = script.RunAsync();scriptReult.Wait();//ITest型の戻り値を取得var obj = scriptReult.Result.ReturnValue;var val = obj.GetValue();
public interface ITest{
int GetValue();}
豆知識 複数回やると同じ名前のがどんどんできるよ。
実行プロセスはアプリと分けておいた方が無難
for (int i = 0; i < 2; i++){
var code = $@"public class Abc{{
public int GetValue()=>{i};}}return new Abc();";
var script = CSharpScript.Create<object>(code);var scriptReult = script.RunAsync();
}
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();Type[] types = assemblies.SelectMany(e =>{
//Roslyn系のDLLを使ってると例外が発生する・・・try{
return e.GetTypes();}catch { }return new Type[0];
}).ToArray();
//二個できているvar count = types.Where(e => e.Name == "Abc").Count();
Expression
λ sql
仲間募集中!
LambdicSqlデモ
https://github.com/Codeer-Software/LambdicSql
Expression
・構文の解析C#の式をデータとして取り込めるこれにより、C#の構文を別の言語に変換できる
・構文作成キャッシュすることによりリフレクションより高速な動的処理が可能
超便利!若干とっつきにくい・・・
Expression解析
int a = 100;Analyze(() => a == 100);
static void Analyze(Expression<Func<bool>> exp)
Func<bool> ではなくExpression<Func<bool>> で受けているところがポイント
EFもこれで受けてます。
C#の構文をDSLとして使える
Db<DB>.Sql(db =>Select(Asterisk()).From(db.tbl_staff).Where(db.tbl_staff.name == "ishikawa"));
SELECT *FROM tbl_staffWHERE tbl_staff.name = @p_0
efModel.tbl_staff.Where(e => e.name == "ishikawa");
SELECT [Extent1].[id] AS [id], [Extent1].[name] AS [name]FROM [dbo].[tbl_staff] AS [Extent1]WHERE 'Jackson' = [Extent1].[name]
Entity Framewrok
LambdicSql
SQLに変換!
ちなみに・・・これは静的には決まらない情報です。
つまり呼び出しコストはタダではない。それが安いか否かは状況によりけり。(通常のSQL呼び出しでは無視できる場面は多い)そのため、渡すExpressionが複雑になればそれだけコストは増していきます。
//呼び出した瞬間に//Expression<Func<bool>>//が生成されるAnalyze(() => a == 100);
ICode Convert(Expression exp){
var method = exp as MethodCallExpression;if (method != null) return Convert(method);
var constant = exp as ConstantExpression;if (constant != null) return Convert(constant);
var binary = exp as BinaryExpression;if (binary != null) return Convert(binary);
var unary = exp as UnaryExpression;if (unary != null) return Convert(unary);
var member = exp as MemberExpression;if (member != null) return Convert(member);
var newExp = exp as NewExpression;if (newExp != null) return Convert(newExp);
var array = exp as NewArrayExpression;if (array != null) return Convert(array);
var memberInit = exp as MemberInitExpression;if (memberInit != null) return Convert(memberInit);
throw new NotSupportedException("Its way of writing is not supported by LambdicSql.");}
LambdicSqlでは以下の式をサポートしてます
クラス 種別 例
ConstantExpression 定数 true
BinaryExpression 二項演算 1 == a
UnaryExpression 単項演算 !value, (boo)obj
MemberExpression メンバ A.B
MethodCallExpression メソッド呼び出し Method(1,2)
NewExpression 生成 new A()
MemberInitExpression 生成時の初期化 new A{ X = 1}
NewArrayExpression param付配列 1, 2, 3
ParameterExpression 引数 ラムダの引数
ConditionalExpressionLambdaExpressionは未対応。それからExpressionで受けれるものだけなので、そもそも制限はある。
LambdicSqlでは以下の式をサポートしてます
木構造なので、再帰的に解析したらOK
ICode Convert(BinaryExpression binary){
//子要素を再帰的に解析していきます。//二項演算式の場合は左右の項目を先に解析する
var left = Convert(binary.Left);var right = Convert(binary.Right);
・・・}
イメージ図
Db<DB>.Sql(db =>Select(Asterisk()).From(db.tbl_staff).Where(db.tbl_staff.name == "Jackson"));
MethodCallExpression(Where)
MethodCallExpression(From)
BinaryExpression(==)
MemberExpression(name)
ConstExpression(“Jackson”)
MemberExpression(tbl_staff)
ParameterExpression(db)
MethodCallExpression(Select)
MemberExpression(tbl_staff)
ParameterExpression(db)
MethodCallExpression(Asterisk)
解析の概要です
【コラム】 括弧がなくなる
Analyze(() => a - (b + c) == d);
a
-
+ d
==
b c
明示的につけた括弧はなくなり最適化されたツリー状態になるのだけど・・・LambdicSqlでどうやっって文字列に戻す?
一番簡単なのは、ツリーを戻すときに両方に括弧を付ける
実動作上は問題ないし、バグることもないでも項目数が増えてくると果てしなくダサい・・・
Analyze(() => a - (b + c) == d);
((@a) – ((@b)+(@c)) = (@d)
【コラム】 括弧がなくなる
static readonly Dictionary<ExpressionType, int> Priority = new Dictionary<ExpressionType, int>
{{ ExpressionType.Or , 0},{ ExpressionType.OrElse , 0},{ ExpressionType.And , 1},{ ExpressionType.AndAlso , 1},{ ExpressionType.LessThan , 2},{ ExpressionType.LessThanOrEqual , 2},{ ExpressionType.GreaterThan , 2},{ ExpressionType.GreaterThanOrEqual , 2},{ ExpressionType.Equal , 3},{ ExpressionType.NotEqual , 3},{ ExpressionType.Add , 4},{ ExpressionType.Subtract , 4},{ ExpressionType.Multiply , 5},{ ExpressionType.Divide , 5},{ ExpressionType.Modulo , 5},
};
static AddingBlankets CheckAddingBlanckets(BinaryExpression binary){
var leftBinary = binary.Left as BinaryExpression;var rightBinary = binary.Right as BinaryExpression;return new AddingBlankets{
Left = (leftBinary != null && Priority[leftBinary.NodeType] < Priority[binary.NodeType]),Right = (rightBinary != null && Priority[rightBinary.NodeType] <= Priority[binary.NodeType])
};}
演算子の優先順位を考える
【コラム】 括弧がなくなる
カッコよくなった!
Analyze(() => a - (b + c) == d);
@a - (@b + @c) = @d
【コラム】 括弧がなくなる
【コラム】 オブジェクトの値取り出し
Db<DB>.Sql(db =>Select(Asterisk()).From(db.tbl_staff).Where(db.tbl_staff.name == target));
SELECT *FROM tbl_staffWHERE tbl_staff.name = @target
Expressionから値を取り出す必要がある。以下の方法が簡単だけど、毎回ビルドはさすがに重い・・・
static object GetObject(MemberExpression exp)=> Expression.Lambda(exp).Compile().DynamicInvoke();
取得用のオブジェクトを一回コンパイルしてそれを使う
Func<object, object> getter =arg => ((ObjClass)arg).target;
これを作ってキャッシュしたい
//arg =>var arg = Expression.Parameter(typeof(object), "arg");
//arg => ((ObjClass)arg)var target = Expression.Convert(arg, memberExp.Expression.Type);
//arg => ((ObjClass)arg).targetvar value = Expression.PropertyOrField(target, memberExp.Member.Name);
//arg => (ojbect)((ObjClass)arg).targetvar converted = Expression.Convert(value, typeof(object));
//コンパイルvar getter = Expression.Lambda<Func<object, object>>(converted, arg).Compile();
【コラム】 オブジェクトの値取り出し
プロパティの元のオブジェクトは?
var constant = memberExp.Expression as ConstantExpression;var method = memberExp.Expression as MethodCallExpression;var newExp = memberExp.Expression as NewExpression;var memberExp2 = memberExp.Expression as MemberExpression;
var constant = memberExp.Expression as ConstantExpression;var obj = constant.Value;
LambdicSqlでは以下の4種類を想定
ConstantExpressionなら値が取れるそれ以外なら再帰的にオブジェクトを取得する
MethodCall,NewExpressionは別途実装。興味があれば、こちらを参照お願いします。https://github.com/Codeer-Software/LambdicSql/blob/master/Project/LambdicSql.Shared/ConverterServices/Inside/ExpressionToObject.cs
まとめ
メタプロのチャンスは色々あるので目的に応じて、用量を守り使ってみましょう!