SpecialNameAttribute クラス (System.Runtime.CompilerServices)
最近見っけた。
これ使うとメソッドやフィールドに IL レベルで specialname 修飾子を付けれる。
using System;
using System.Runtime.CompilerServices;
class Hoge
{
public Hoge(string value)
{
_value = value;
}
private string _value;
[SpecialName]
public static string op_Explicit(Hoge obj)
{
return obj._value;
}
}
こうすれば普通のメソッド定義と同じ形式で演算子のオーバーロードができる。
けど、同一プロジェクト内からは演算子として呼び出すことはできなくて、静的メソッドとして呼び出す必要あり。
これを使って何か面白いことできないかなーとか考えてたんだけど・・・無い!
.ctor なんて名前のメソッドは定義できないからコンストラクタは作れないし、get_XXX とか set_XXX とか add_XXX とか remove_XXX なんてメソッド作ったところでプロパティやイベントになるわけでもなし。まぁできたところで・・・何も面白くない。
interface との変換演算子とかジェネリックな変換演算子なんて作ってみたけどコンパイラが認識しないから実行されず。
強いて言えば C++/CLI で使える非静的な演算子を用意したりできるけど・・・無意味すぎ。
こんな感じ。
class Hoge
{
}
class Fuga : Hoge
{
}
class Piyo : Hoge
{
}
class Program
{
static void Main(string[] args)
{
var hoge1 = new Hoge();
var hoge2 = new Hoge();
var fuga1 = new Fuga();
var fuga2 = new Fuga();
var piyo = new Piyo();
bool b1 = hoge1.Equals(hoge2); // T = Hoge
bool b2 = hoge1.Equals(fuga1); // T = Hoge
bool b3 = fuga1.Equals(fuga2); // T = Fuga
bool b4 = fuga1.Equals(hoge1); // T = Hoge (!!)
//bool b5 = fuga1.Equals(piyo); // Build Error !!
bool b6 = fuga1.Equals((Hoge)piyo); // T = Hoge
bool b7 = fuga1.Equals<Hoge>(piyo); // T = Hoge
}
}
[検証プログラム]
インテリセンスに Object.Equals(object) が出てこないこと、Equals の引数の型が自分自身になることを確認できます。
ビルドが出来るだけで実行は出来ません。
ダウンロードページ
public class Object
{
protected internal virtual bool Equals(object obj)
{
...
}
}
public static class ObjectExtension
{
public static bool Equals<T>(this T source, T obj)
{
if (source == null)
{
// return (obj == null); にするのも面白い
throw new NullReferenceException();
}
return source.Equals((object)obj);
}
}
あまり深く考えずに言っているので、なんらかの不都合が生じるかもしれないけど。。。
つか、拡張メソッドのサポートを全ての言語に強いることになるからダメかな。
つかつか、やっぱし自分自身の型を示すキーワードが欲しくなる。
.NET クラスライブラリ探訪-038 (System.Tuple)(タプル, 組オブジェクト, 4.0から追加されたクラス) - いろいろ備忘録日記
個人的には以前 今更 Func デリゲートや Action デリゲートについて一言 に書いた理屈と同様に、Tuple も無闇には使わずクラス内部のみといった限定的な利用に留めておくのが良いと考えていますが、Tuple の活用シーンは結構あるのではないかと思います。
Tuple は配列と少し似ていますが、次のような違いがあります。
- 長さ毎に型が用意されている
- 各要素は厳密に型付けされる
- 各要素は読み取り専用
- Tuple の Equals メソッドは各要素の等値性を比較する
- Tuple の GetHashCode メソッドは各要素を元にハッシュコードを導出する
Tuple は配列やコレクションと違って、同一性ではなく等値性で区別されます。これにより、Tuple を Dictionary の複合キーとして使用することができます。
private Dictionary<Tuple<int, int>, string> _dict;
public string GetValue(int key1, int key2)
{
return this._dict[Tuple.Create(key1, key2)];
}
また、自作クラス/構造体の Equals メソッド及び GetHashCode メソッドをオーバーライドする時、全てのフィールドをセットした Tuple に処理を委譲することで実装を容易にすることができます。
public sealed class Class1
{
public Class1(int arg1, string arg2)
{
this._field1 = arg1;
this._field2 = arg2;
}
private readonly int _field1;
private readonly string _field2;
public override bool Equals(object other)
{
var otherAsClass1 = other as Class1;
if (otherAsClass1 == null)
{
return false;
}
var thisFieldTuple = this.CreateFieldTuple();
var otherFieldTuple = otherAsClass1.CreateFieldTuple();
return thisFieldTuple.Equals(otherFieldTuple);
}
public override int GetHashCode()
{
var fieldTuple = this.CreateFieldTuple();
return fieldTuple.GetHashCode();
}
private Tuple<int, string> CreateFieldTuple()
{
return Tuple.Create(this._field1, this._field2);
}
}
ということで、Tuple は使い方次第でかなり重宝するかもしれません。
ということは、これらは CER 内に含められないし、呼び出し元は Cer.None になってしまう、ということだろーか…?
本来、GC.AddMemoryPressure には Cer.MayFail か Cer.Success が、GC.RemoveMemoryPressure には Cer.Success がマークされてるべきじゃないの??
null許容型は参照型と値型のどちらになるの?
C# だとローカル変数への代入は値渡ししかできません。
参照渡しで代入を行うようなコードを C# っぽく書いてみます。
Sample.cs
class Program
{
static void Main()
{
int v0 = 0;
int& v1 = v0;
v0 = 5;
System.Console.WriteLine(v0);
System.Console.WriteLine(v1);
}
}
このコードが動作するとすれば、v1 への代入が参照渡しになるので v1 からも 5 が取得されるはずですが、当然ながらこのコードはそもそもコンパイルできません。
しかし、IL ならばローカル変数への参照渡しができます。
先ほどのコードと同等のコードを IL で書くと次のようになります。
Sample.il
.assembly extern mscorlib { }
.assembly sample { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.locals init
(
[0] valuetype [mscorlib]System.Int32 v0,
[1] valuetype [mscorlib]System.Int32& v1
)
// v0 = 0;
ldc.i4.0
stloc.0
// v1 = v0;
ldloca.s v0
stloc.1
// v0 = 5;
ldc.i4.5
stloc.0
// Console.WriteLine(v0);
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
// Console.WriteLine(v2);
ldloc.1
ldind.i4
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
このコードを次のコマンドで exe ファイルとしてアセンブルします。
ilasm Sample.il
これを実行すると、v0 も v1 も 5 を返すことが確認できます。
つまり、v1 への参照渡しによる代入が可能であることが確認できます。
…まぁ、だからどうというわけでもないんですが。
IL で可能なので、C++/CLI でもできるかもしれませんね。
独自のデリゲートを定義する代わりになんでもかんでも Func デリゲートやら Action デリゲートで済ますのはやめましょう。
これらのデリゲートは汎用的に使用できるようにと定義されていますが、大きな欠点があります。それは、パラメータに名称を付けられないということです。
例えば Enumerable.Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Int32, TResult> selector) メソッドは、Func<TSource, Int32, TResult> デリゲートを受け取ります。この Func デリゲートの第一引数はまぁ良いとして (※)、第二引数は一体何の役割を持っているのでしょうか。
前述の通り、Func デリゲートではパラメータに名称を付けられないため、引数の役割はドキュメントから読み取るしかありません。
ドキュメントを読めば、シーケンス内での要素の位置を表す値であることがわかりますが、これがもし、Func デリゲートではなく次のように厳密なデリゲートだったとしたらどうでしょう。
public delegate TSource Selector<TSource>(TSource source, int index);
これならば、ドキュメントを読む前に index という名前から第二引数の役割を推測できます。しかも第二引数の詳細を知りたければ、直接このデリゲートのドキュメントを読めば済みます。
// 追記 (2009/05/24)
パラメータ名がないとドキュメントの書き方が「第一引数は○○です」なんて説明になるので、パラメータリストの順序変更などでドキュメントとの不整合が生じやすくもなります。しっかりデリゲー ト定義すれば <param> タグが使えるので、ドキュメントとの不整合もある程度コンパイラが警告してくれます。
// 追記ここまで
というわけで、Func デリゲートやら Action デリゲートを全否定するわけではないですが、これらのデリゲートは表現力が弱いということは認識しておいた方が良いと思います。
※ これに関しては Select メソッドの型パラメータ名 (TSource) によって役割を推測できます。でも Enumerable.Aggregate<TSource>(IEnumerable<TSource> source, Func<TSource, TSource, TSource> func) なんかだと Aggregate メソッドの型パラメータ名 (TSource) からも推測できないです。
直交座標系とか極座標系とかはよくわかってないけど、とりあえず C# でもできます。
まぁ、2つのインスタンスのメモリ上のサイズが等しくない場合はマズいことになりかねませんけどね…。
Cartesian と Polar はサイズが等しいので大丈夫です。
// 追記1 (2009/01/28)
対象オブジェクトのフィールドを辿るとマネージヒープ上のオブジェクトへの参照が含まれている、という場合も、場合によってはマズいことになります。
// 追記2 (2009/01/28)
NyaRuRu さんより、「何が起きても不思議ではない」とご指摘頂きました。
今回のコードは動作しましたが、インスタンスサイズを揃えて、フィールドにマネージヒープ上のオブジェクトへの参照を含めないようにしたとしても、確実に大丈夫だと断言することはできません。また、今回のコードが如何なる時でも確実に動作すると断言することもできません。
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
class Cartesian
{
public Cartesian(double x, double y)
{
this.X = x;
this.Y = y;
}
public double X { get; set; }
public double Y { get; set; }
public Polar ToPolar()
{
return new Polar(Math.Sqrt(X * X + Y * Y), Math.Atan2(Y, X));
}
}
class Polar
{
public Polar(double r, double theta)
{
this.R = r;
this.Theta = theta;
}
public double R { get; set; }
public double Theta { get; set; }
public Cartesian ToCartesian()
{
return new Cartesian(R * Math.Cos(Theta), R * Math.Sin(Theta));
}
}
class Program
{
static void Transmogrify(object a, object b)
{
Type aType = a.GetType();
Type bType = b.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo[] aFieldInfos = aType.GetFields(flags);
FieldInfo[] bFieldInfos = bType.GetFields(flags);
var aFields = aFieldInfos.Select(item => new { Key = item, Value = item.GetValue(a) }).ToList();
var bFields = bFieldInfos.Select(item => new { Key = item, Value = item.GetValue(b) }).ToList();
ConvertType(a, bType);
ConvertType(b, aType);
aFields.ForEach(item => item.Key.SetValue(b, item.Value));
bFields.ForEach(item => item.Key.SetValue(a, item.Value));
}
static unsafe void ConvertType(object target, Type newType)
{
IntPtr newTypeHandle = newType.TypeHandle.Value;
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
void* targetPointer = *((void**)entryPointer);
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
*typeHandlePointer = newTypeHandle;
}
finally
{
targetGCHandle.Free();
}
}
static void Main(string[] args)
{
Polar pos1 = new Polar(Math.Sqrt(2), Math.PI / 4);
Polar pos2 = pos1;
Console.WriteLine(pos1.GetType().Name); //=> Polar
Transmogrify(pos1, pos1.ToCartesian());
Console.WriteLine(pos1.GetType().Name); //=> Cartesian
Cartesian pos1AsCart = pos1 as object as Cartesian;
Console.WriteLine(pos1AsCart.X); //=> 1
Console.WriteLine(pos1AsCart.Y); //=> 1
Console.WriteLine(pos2.GetType().Name); //=> Cartesian
Transmogrify(pos1, pos1AsCart.ToPolar());
Console.WriteLine(pos1.GetType().Name); //=> Polar
Console.WriteLine(pos1.R); //=> 1.4142135623731
Console.WriteLine(pos1.Theta / Math.PI); //=> 0.25
Console.ReadLine();
}
}
[関連]
C#と諸々 オブジェクトの型を破壊的に変換
C#と諸々 ガベージコレクションを開始するには
例えば、次のコードの実行結果を Debug ビルドと Release ビルド (非デバッグ実行) とで比較すると一目瞭然だ。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
Debug ビルドでは Hoge と出力されるが、Release ビルド (非デバッグ実行) では Main と出力されることが確認できる。つまり、Release ビルド (非デバッグ実行) では JIT 最適化により Hoge メソッドがインライン展開されているわけである。
JIT 最適化によるインライン展開を抑止する方法としては、MethodImpl 属性 を付加して MethodImplOptions.NoInlining を指定するという方法が提供されている。しかし、この方法でインライン展開が抑止されるのは属性が付加されたメソッドのみである。そのため、呼び出し元のメソッド (上記の例なら Hoge メソッド) にこの属性を付加しなければならない。
また、この属性は絶対的なものではなく、64 bit CLR では無視されてしまうらしい。
また、インライン展開の抑止は、C# ・ IL レベルのメソッド呼び出しとコールスタックの一致を保証するわけではない。64 bit - CLR では、インライン展開以外に末尾最適化によっても、メソッド呼び出しとコールスタックの不一致が生じる場合があるのだが、NoInlining では末尾最適化は抑止されない。
デバッグ技 : .ini ファイルによる JIT コンパイラ制御
# このリンク先では ini ファイルを使用して インライン展開を抑止する方法 コールスタックの不一致 を防ぐ方法が紹介されているが、これはあくまでもデバッグ目的の手段である。
では、呼び出し元メソッドのコールスタックを維持する方法は無いだろうか。
実はある。
IL レベルで reqsecobj キーワードが付加されているメソッドでは、呼び出し元メソッドのコールスタックが維持されるのである。 (reqsecobj キーワードが付加されているメソッド自身のコールスタックも維持される。)
C# コンパイラには、このキーワードをメソッドに付加する方法が用意されていて、メソッドに DynamicSecurityMethodAttribute クラス (System.Security) を属性として付加すれば良い。
このクラスは mscorlib.dll の internal クラスであり僕らは本来使用することができないのだが、同名のクラスを自前で用意して使用すれば C# コンパイラは reqsecobj キーワードを付加してくれる。
先ほどのコードに、DynamicSecurity 属性を付けて、再び Release ビルド (非デバッグ実行) で実行してみると、見事にコールスタックが維持されることが確認できる。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[DynamicSecurityMethod]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
namespace System.Security
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
internal sealed class DynamicSecurityMethodAttribute : Attribute
{
}
}
見ての通り、呼び出し元メソッドである Hoge メソッドには属性の付加が一切不要である。
[修正履歴]
2008/10/07
NyaRuRu さんのコメントを元に記事本文を修正。
重要な削除箇所は取り消し線 + 文字色変更 (灰色)。
重要な追記箇所は背景色変更 (緑色)。
目印は付けていないが、何箇所か「インライン展開が抑止」という旨の記述を「コールスタックが維持」という記述に修正してある。
2008/10/08
冒頭の検証用コードで、Fuga メソッドまでインライン展開されてしまいエラーとなっていたので 、Fuga メソッドに MethodImpl(MethodImplOptions.NoInlining) 属性を付加。