ちょっとレアな プログラミング Tips
少し珍しめの Tips をご紹介しますので、よろしければ開発にお役立てください。
[+]すべて展開
[-]すべて縮小
// 以下の例で使用する変数
DateTime date = DateTime.Today;
▼前月初日
new DateTime(date.Year, date.Month, 1).AddMonths(-1)
▼前月同日
date.AddMonths(-1)
▼前月末日
new DateTime(date.Year, date.Month, 1).AddDays(-1)
▼前々月末日
new DateTime(date.Year, date.Month, 1).AddMonths(-1).AddDays(-1)
▼当月初日
new DateTime(date.Year, date.Month, 1)
▼当月末日
new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month))
▼翌月初日
new DateTime(date.Year, date.Month, 1).AddMonths(1)
▼翌月末日
new DateTime(date.Year, date.Month, 1).AddMonths(2).AddDays(-1)
▼1年前の前月初日
new DateTime(date.Year, date.Month, 1).AddYears(-1).AddMonths(-1)
▼1年前の前月末日
new DateTime(date.Year, date.Month, 1).AddYears(-1).AddDays(-1)
▼1年前の当月初日
new DateTime(date.Year, date.Month, 1).AddYears(-1)
▼4月開始年度
date.AddMonths(-3).Year
値型の等価判定には気をつけるべき点があります。
以下の例で、Assert.IsTrue なら () 内が 真、Assert.IsFalse なら () 内が 偽です。
★印の箇所は注意が必要です。
Object 型にボックス化した値を比較します。
int i = 1;
object o1 = i;
object o2 = i;
// Object.Equals が int の Equals を呼び出すので値で比較されます。
Assert.IsTrue(o1.Equals(o2));
// o1 != o2 かつ o1 != null なら上の o1.Equals(o2) と同じ結果となります。
Assert.IsTrue(Object.Equals(o1, o2));
ここまではいいでしょう。
次は少し注意が必要です。
// ★ボックス化(値型 → object)すると参照比較になるので一致しません。
Assert.IsFalse(o1 == o2);
// ★引数として渡されるときにボックス化されるので一致しません。
Assert.IsFalse(Object.ReferenceEquals(1, 1));
Assert.IsFalse(Object.ReferenceEquals(i, i));
DataRow の各フィールド値(インデクサ/Itemプロパティ)なども Object 型なので、比較の際は注意しましょう。
今度は異なる型を比較します。
long l = 1;
// 比較演算子では、int が long に暗黙的に型変換され、long 同士として比較されます。
Assert.IsTrue(i == l);
Assert.IsTrue(l == i);
// 暗黙の型変換ができるなら Equals で比較できます。
Assert.IsTrue(l.Equals(i));
ここまではいいでしょう。
以降は注意が必要です。
// ★暗黙の型変換ができないと Equals は false を返します。
// コンパイルは通り、例外も発生しないので見逃しがちです。
Assert.IsFalse(i.Equals(l));
// ★異なる型へのボックス化解除は失敗します。
object iBoxed = i;
try
{
// int 型をボックス化しているので long 型に戻すことはできません。(InvalidCastException が発生)
bool b = (l == (long)iBoxed);
Assert.Fail();
}
catch (InvalidCastException) {}
おまけです。
// 実行されるのは Object.Equals です。
// 字面だけ見ると true を期待してしまいそう(?)です。
Assert.IsFalse(Type.Equals(1, 2));
[値型を自作する場合の注意点]
値型(struct/Structure)の Equals メソッドは、既定ではリフレクションを使用して全フィールド(自動実装プロパティのバッキングフィールドを含む)を定義された型の Equals メソッドで比較します。
値型を自分で作成する場合、Equals メソッドをオーバーライドすることで、この比較処理をカスタマイズすることができます。
等価演算子(==)を使用するためには、「==」「!=」のオーバーロードを定義する必要があります。
値型を定義する場合には、Equals メソッドのオーバーライドと等価演算子のオーバーロードが推奨されています。
⇒ コード分析(FxCop)「CA1815: equals および operator equals を値型でオーバーライドします」
Color 構造体の等価判定には少し癖があります。
以下の例で、Assert.IsTrue なら () 内が 真、Assert.IsFalse なら () 内が 偽です。
Assert.IsTrue(Color.Red == Color.FromName("Red"));
Assert.IsTrue(Color.Red == Color.FromKnownColor(KnownColor.Red));
ここまでは期待どおりです。
Color redFromArgb = Color.FromArgb(Color.Red.A, Color.Red.R, Color.Red.G, Color.Red.B);
Assert.IsFalse(Color.Red == redFromArgb);
Assert.IsFalse(Color.Red.Equals(redFromArgb));
ARGBをあわせたのに等価と判定されません。
なぜでしょう。
プロパティを見てみましょう。
// Name
Assert.AreEqual("Red", Color.Red.Name);
Assert.AreEqual("ffff0000", redFromArgb.Name);
// IsKnownColor
Assert.IsTrue(Color.Red.IsKnownColor);
Assert.IsFalse(redFromArgb.IsKnownColor);
// IsNamedColor
Assert.IsTrue(Color.Red.IsNamedColor);
Assert.IsFalse(redFromArgb.IsNamedColor);
違いが表れました。
これらのプロパティ値は実際の色がどうであるかではなく、Color インスタンスがどのようにして生成されたかによって変わります。
内部的には state という private フィールドに区分が格納され、オーバーライドされた Equals メソッドで比較に使用されます。
実際に表示される色が同じですので、ARGB値で比較すると等価と判定されます。
Assert.IsTrue(Color.Red.ToArgb() == redFromArgb.ToArgb());
次に浮動小数点型を見てみましょう。
double 型には非数を表す NaN という値があります。
NaN (Not a Number) 自体は .NET 独自のものでなく、IEEE 754(浮動小数点数算術標準)で定められています。
NaN は IsNaN メソッドを使って判定することができます。
Assert.IsTrue(double.IsNaN(double.NaN));
0 を浮動小数点型の 0 で割ると NaN になります。
(int や decimal の 0 で割ったときと異なり、DivideByZeroException は発生しません)
double zero = 0;
Assert.IsTrue(double.IsNaN(0 / zero));
ちなみに正の値を浮動小数点型の 0 で割ると PositiveInfinity に、負の値を割ると NegativeInfinity になります。
Assert.IsFalse(double.IsNaN(1 / zero));
Assert.IsTrue(double.IsPositiveInfinity(1 / zero));
Assert.IsFalse(double.IsNaN(-1 / zero));
Assert.IsTrue(double.IsNegativeInfinity(-1 / zero));
非数に対する計算操作の結果もまた非数となります。
Assert.IsTrue(double.IsNaN(double.NaN + 1));
Assert.IsTrue(double.IsNaN(Math.Floor(double.NaN)));
非数は数ではありませんので、演算子は NaN 自身も含め、どの値とも等価でないと判断します。
Assert.IsFalse(double.NaN == 0);
Assert.IsFalse(double.NaN < 0);
Assert.IsFalse(double.NaN >= 0);
Assert.IsFalse(double.NaN == double.NaN);
Assert.IsFalse(double.NaN < double.NaN);
Assert.IsFalse(double.NaN >= double.NaN);
すべての演算結果が false になるというわけではありません。
非等値演算子は NaN 自身との比較においても true を返しますので注意が必要です。
Assert.IsTrue(double.NaN != 0);
Assert.IsTrue(double.NaN != double.NaN);
演算子と異なり、比較メソッドでは同値判定が可能です。
Assert.IsTrue(double.NaN.Equals(double.NaN));
Assert.AreEqual(0, double.NaN.CompareTo(double.NaN));
Assert.IsFalse(double.NaN.Equals(0));
Assert.AreEqual(-1, double.NaN.CompareTo(0));
float 型にも NaN があり、double.NaN と値は同じですが、通常の値の場合と同様、暗黙の型変換ができないと Equals が false を返します。
Assert.IsFalse(double.NaN == float.NaN);
Assert.IsTrue(double.NaN.Equals(float.NaN));
// float ← double は暗黙の型変換不可
Assert.IsFalse(float.NaN.Equals(double.NaN));
浮動小数点型では精度も問題になります。
double precision15 = .333333333333333;
double precision17 = 1.0 / 3;
書式 "R" をつけて最大17桁まで確認してみると、有効桁数が異なることがわかります。
Assert.AreEqual("0.333333333333333", precision15.ToString("R"));
Assert.AreEqual("0.33333333333333331", precision17.ToString("R"));
有効桁数が異なるため、等価にはなりません。
Assert.IsFalse(precision15 == precision17);
Assert.IsFalse(precision15.Equals(precision17));
有効桁数を Math.Round で揃えると等価と判定されます。
Assert.IsTrue(Math.Round(precision15, 15) == Math.Round(precision17, 15));
Assert.IsTrue(Math.Round(precision15, 15).Equals(Math.Round(precision17, 15)));
最後は列挙型です。
メンバとして定義していない値が格納された場合の等価判定について、テストコード中のコメントで解説させていただきます。
// 列挙型定義
private enum ZeroUndefined
{
One = 1,
Two
}
// フィールド
private ZeroUndefined zeroUndefinedUnassigned;
// 検証
public void EnumUndefinedMember()
{
// 0 はメンバとして定義されていません。
Assert.IsFalse(Enum.IsDefined(typeof(ZeroUndefined), 0));
// ★既定値はメンバの定義がなくても 0 です。 default(T) もフィールドも同様です。
Assert.AreEqual((ZeroUndefined)0, default(ZeroUndefined));
Assert.AreEqual((ZeroUndefined)0, this.zeroUndefinedUnassigned);
// ★リテラルや定数の 0 は特別に列挙型への暗黙変換が許可されているため、キャストなしで代入できます。
ZeroUndefined undefinedZero = 0;
/* int 型変数の代入はコンパイルエラーになります。
int zero = 0;
ZeroUndefined undefinedZero = zero;
*/
// 0 はどのメンバとも一致しません。
foreach (ZeroUndefined each in Enum.GetValues(typeof(ZeroUndefined)))
{
Assert.IsTrue(undefinedZero != each);
}
// int にキャストすると 0 になります。
Assert.IsTrue((int)undefinedZero == 0);
// 0 をキャストしたものと一致します。
Assert.IsTrue(undefinedZero == (ZeroUndefined)0);
// ★リテラルや定数の 0 は特別に列挙型への暗黙変換が許可されているため、キャストなしで等価比較できます。
Assert.IsTrue(undefinedZero == 0);
Assert.IsTrue(0 == undefinedZero);
// ★int 型との Equals 比較は false になります。
Assert.IsFalse(undefinedZero.Equals(0));
Assert.IsFalse(0.Equals(undefinedZero));
// 最大定義値を超える場合も数値は保持されます。
ZeroUndefined maxValue = (ZeroUndefined)int.MaxValue;
Assert.IsTrue(maxValue == (ZeroUndefined)int.MaxValue);
Assert.IsTrue((int)maxValue == int.MaxValue);
}
※FlagsAttribute 属性が付与されていない列挙型では、値ゼロのメンバを定義することが推奨されています。
⇒ コード分析(FxCop)「CA1008: Enums は 0 値を含んでいなければなりません」
参照型の等価比較には、大きく分けて参照の比較と値の比較があります。
Object.ReferenceEquals メソッドや VB.NET の Is 演算子を使うと参照の比較になります。
等価演算子 '=='、Equals メソッドは Object 型の既定実装は参照の比較ですが、等価演算子はオーバーロード、Equals メソッドはオーバーライド/オーバーロード(もあることに注意)という方法で型独自の振る舞いを実装することができますので、注意が必要です。
Microsoft Docs の「参照型のガイドライン」では、型がイミュータブルな(変更できない)値を表現する場合、Equals メソッドのオーバーライドを検討することが推奨されていますが、ほとんどの場合において等価演算子 '==' はオーバーロードすべきでない、とされています。
ここでは String クラスと StringBuilder クラスを例に、等価判定の動作を検証してみます。
※以下、Assert.IsTrue なら () 内が 真、Assert.IsFalse なら () 内が 偽です。
※★印の箇所は注意が必要です。
▼String 型 の例
string s1 = new string(new char[] {'s'});
string s2 = new string(new char[] {'s'});
object s1AsObj = s1;
object s2AsObj = s2;
/*【等価演算子】*/
// String 型同士の場合、値比較となります。
// 等価演算子がオーバーロードされています。
Assert.IsTrue(s1 == s2);
// ★どちらかが Object 型に収まっている場合、参照比較となります。
// 等価演算子は Object 型のものが使用されます。
Assert.IsFalse(s1AsObj == s2AsObj);
Assert.IsFalse(s1 == s2AsObj);
Assert.IsFalse(s2AsObj == s1);
/*【Equals メソッド】*/
// String 型同士の場合、値比較となります。
// String 型引数のオーバーロードが使用されます。
Assert.IsTrue(s1.Equals(s2));
// どちらかが Object 型に収まっている場合も値比較となります。
// Object.Equals(object) のオーバーライドが使用されます。
Assert.IsTrue(s1.Equals(s2AsObj));
// Object 型のメソッド呼び出しでもオーバーライドされた Equals メソッドが使用されます。
Assert.IsTrue(s2AsObj.Equals(s1));
/*【Object.Equals 静的メソッド】*/
// 値比較となります。
// 中からオーバーライドされた Equals(object) メソッドが呼ばれます。
Assert.IsTrue(Object.Equals(s1, s2));
Assert.IsTrue(Object.Equals(s1AsObj, s2AsObj));
/* 《参考》Object.Equals の実装
public static bool Equals(object objA, object objB)
{
return ((objA == objB) || (((objA != null) && (objB != null)) && objA.Equals(objB)));
}
*/
/*【Object.ReferenceEquals 静的メソッド】*/
// 同じインスタンスなら一致します。
Assert.IsTrue(Object.ReferenceEquals(s1, s1));
Assert.IsTrue(Object.ReferenceEquals(s1, s1AsObj));
// インスタンス異なれば、値が同じでも一致しません。
Assert.IsFalse(Object.ReferenceEquals(s1, s2));
Assert.IsFalse(Object.ReferenceEquals(s1AsObj, s2AsObj));
// 文字列のリテラルは同一値に同じ参照が使用されます。
// 「インターンプール」というテーブルに保持されます。
// ※「インターンプール」については近日公開予定の特殊編で詳しく解説します。
Assert.IsTrue(Object.ReferenceEquals("s", "s"));
string s1FromLiteral = "s";
Assert.IsTrue(Object.ReferenceEquals("s", s1FromLiteral));
// リテラルから生成されていない値とは一致しません。
Assert.IsFalse(Object.ReferenceEquals("s", s1));
▼StringBuilder 型の例
StringBuilder sb1 = new StringBuilder("sb");
var sb2 = new StringBuilder("sb");
var sb1Assigned = sb1;
object sb1AsObj = sb1;
object sb2AsObj = sb2;
string sb1AsStr = sb1.ToString();
/*【等価演算子】*/
// Object 型の等価演算子で参照比較となります。
Assert.IsFalse(sb1AsObj == sb2AsObj);
// ★StringBuilder 型同士でも参照比較となります。を指しています。
// ガイドラインに沿って等価演算子がオーバーロードされていません。
// 下の Equals メソッドとの違いに注意が必要です。
Assert.IsFalse(sb1 == sb2);
/* 「演算子 '==' を 'string' と 'System.Text.StringBuilder' 型のオペランドに適用することはできません。」
* コンパイルエラーになるため、ToString() の漏れなどミスに気づきます。
Assert.IsFalse(sb1AsStr == sb1);
*/
/*【Equals メソッド】*/
// ★StringBuilder 型同士の場合、値比較となります。
// StringBuilder 型引数のオーバーロードが使用されます。
// Equals(object) メソッドが呼ばれているわけではないことに注意しましょう。
Assert.IsTrue(sb1.Equals(sb2));
// ★どちらかが Object 型の場合、参照比較となります。
// Object.Equals(object) メソッドはオーバーライドされていません。
// 上のオーバーロードとの違いに注意が必要です。
// まさに落とし穴的ですが、StringBuilder の表す値はミュータブル(変更可能)ですので、オーバーロード定義の是非はともかく、Equals(object) メソッドをオーバーライドしないこと自体はガイドラインに沿っています。
Assert.IsFalse(sb1.Equals(sb2AsObj));
// ★ToString() の付け忘れに注意しましょう。
// 型が違ってもコンパイルが通ってしまい、常に false を返すことになります。
Assert.IsFalse(sb1AsStr.Equals(sb1));
// 文字列同士なら一致します。
Assert.IsTrue(sb1AsStr.Equals(sb1.ToString()));
/*【Object.Equals 静的メソッド】*/
// ★参照比較となります。
// 中からオーバーライドされていない Equals(object) メソッドが呼ばれます。
// 上の Equals(StringBuilder) オーバーロードとの違いに注意が必要です。
Assert.IsFalse(Object.Equals(sb1, sb2));
// 代入された場合は一致します。
Assert.IsTrue(Object.Equals(sb1, sb1Assigned));
/*【Object.ReferenceEquals 静的メソッド】*/
// いずれも参照比較です。
Assert.IsTrue(Object.ReferenceEquals(sb1, sb1));
Assert.IsFalse(Object.ReferenceEquals(sb1, sb2));
Assert.IsTrue(Object.ReferenceEquals(sb1, sb1Assigned));
文字列のリテラルは「インターンプール」というテーブルに保持され、同一値に同じ参照が使用されます。
String.Intern メソッドでそれらの参照を取り出すことができます。
string literal = "CodeOne";
// 同一値のリテラルは参照も一致します。
Assert.IsTrue(Object.ReferenceEquals(literal, "CodeOne"));
// 値は同一でも、リテラルと連結された文字列では参照が異なります。
string built = new StringBuilder().Append("Code").Append("One").ToString();
Assert.IsTrue(Object.Equals(literal, built));
Assert.IsFalse(Object.ReferenceEquals(literal, built));
// インターンプールから取得すると、同一値のリテラルと参照が一致します。
Assert.IsTrue(Object.ReferenceEquals(literal, String.Intern(built)));
※Ngen.exe でアセンブリをコンパイルした場合など、インターンプールに格納されない場合もあります。
《参考》String.Intern メソッド | Microsoft Docs
匿名型では、Equals, GetHashCode メソッドのオーバーライドがコンパイラによって生成されます。
プロパティの数や順序によっても結果が異なりますので注意が必要です。
var staffA = new { Id = 1, Name = "山田" };
var staffB = new { Id = 1, Name = "山田" };
// インスタンスが同じなら参照比較は一致します。
Assert.IsTrue(staffA == staffA);
Assert.IsTrue(Object.ReferenceEquals(staffA, staffA));
// プロパティの数と順序、値が同じでも、インスタンスが異なれば参照比較は一致しません。
Assert.IsFalse(staffA == staffB);
Assert.IsFalse(Object.ReferenceEquals(staffA, staffB));
// プロパティの数と順序、値が同じ場合、Equals の結果は true になります。
Assert.IsTrue(staffA.Equals(staffB));
Assert.IsTrue(staffA.Equals((object)staffB));
Assert.IsTrue(Object.Equals(staffA, staffB));
// プロパティの順序が異なる場合、Equals の結果は false になります。
var staffC = new { Name = "山田", Id = 1 };
Assert.IsFalse(staffA.Equals(staffC));
// プロパティの数が異なる場合、Equals の結果は false になります。
var staffD = new { Id = 1, Name = "山田", Role = "開発者" };
var staffE = new { Id = 1, Name = "山田", Role = (string)null };
Assert.IsFalse(staffA.Equals(staffD));
Assert.IsFalse(staffA.Equals(staffE));
VB.NET の匿名型では、Key キーワードによって Equals 比較に使用するプロパティを指定できます。
プロパティの数や順序に Key キーワードが絡みますので、C# よりさらに複雑となります。
Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffB = New With { Key .Id = 1, Key .Name = "山田" }
'インスタンスが同じなら参照比較は一致します。
Assert.IsTrue(staffA Is staffA)
Assert.IsTrue(Object.ReferenceEquals(staffA, staffA))
'プロパティの数と順序、値が同じでも、インスタンスが異なれば参照比較は一致しません。
Assert.IsFalse(staffA Is staffB)
Assert.IsFalse(Object.ReferenceEquals(staffA, staffB))
'プロパティの数と順序、値が同じですべて Key キーワード付きの場合、Equals の結果は True になります。
Assert.IsTrue(staffA.Equals(staffB))
Assert.IsTrue(staffA.Equals(DirectCast(staffB, Object)))
Assert.IsTrue(Object.Equals(staffA, staffB))
'プロパティの順序が異なる場合、Equals の結果は False になります。
Dim staffC = New With { Key .Name = "山田", Key .Id = 1 }
Assert.IsFalse(staffA.Equals(staffC))
'プロパティの数が異なる場合、Equals の結果は False になります。
Dim staffD = New With { Key .Id = 1, Key .Name = "山田", Key .Role = "開発者" }
Assert.IsFalse(staffA.Equals(staffD))
'プロパティの数と順序、値が同じでも、一方に Key キーワードが漏れていると Equals の結果は False になります。
Dim staffE = New With { Key .Id = 1, .Name = "山田" }
Assert.IsFalse(staffA.Equals(staffE))
'プロパティの数は Key キーワードなしも含めて一致しないと Equals は True を返しません。
Dim staffF = New With { Key .Id = 1, Key .Name = "山田", .Role = "開発者" }
Assert.IsFalse(staffA.Equals(staffF))
'Key キーワードの付いていないプロパティは Equals 判定に使用されません。
Dim staffG = New With { Key .Id = 1, Key .Name = "山田", .Role = "開発者" }
Dim staffH = New With { Key .Id = 1, Key .Name = "山田", .Role = "営業担当者" }
Assert.IsTrue(staffG.Equals(staffH))
'Key キーワード付きのプロパティが1つもない場合、Equals は参照比較となります。
Dim staffI = New With { .Id = 1, .Name = "山田" }
Dim staffJ = New With { .Id = 1, .Name = "山田" }
Assert.IsTrue(staffI.Equals(staffI))
Assert.IsFalse(staffI.Equals(staffJ))
言語によって null や空文字の扱いは異なることがあります。
以下に例を示します。
判定 |
C# |
VB.NET |
JavaScript(厳密/非厳密) |
SQL(ANSI/ISO準拠) |
Equals |
Equals(null, null) true |
Equals(Nothing, Nothing) True |
― |
― |
Is |
― |
(Nothing Is Nothing) True |
Object.is(null, null) true |
(NULL IS NULL) TRUE |
Is Not |
― |
(Nothing IsNot Nothing) False |
― |
(NULL IS NOT NULL) FALSE |
null との等号比較 |
(null == null) true |
(Nothing = Nothing) True |
(null === null) true (null == null) true |
(NULL = NULL) UNKNOWN NOT (NULL = NULL) UNKNOWN ※NOT しても UNKNOWN(以下同様) |
null との不等号比較 |
(null != null) false |
(Nothing <> Nothing) False |
(null !== null) false (null != null) false |
(NULL <> NULL) UNKNOWN |
空文字との等号比較 |
(null == "") false |
(Nothing = "") True |
(null === "") false (null == "") false |
(NULL = '') UNKNOWN |
空文字との不等号比較 |
(null != "") true |
(Nothing <> "") False |
(null !== "") true (null != "") true |
(NULL <> '') UNKNOWN |
有効文字との等号比較 |
(null == "a") false |
(Nothing = "a") False |
(null === "a") false (null == "a") false |
(NULL = 'a') UNKNOWN |
有効文字との不等号比較 |
(null != "a") true |
(Nothing <> "a") True |
(null !== "a") true (null != "a") true |
(NULL <> 'a') UNKNOWN |
未定義値との等号比較 |
― |
― |
(null === undefined) false (null == undefined) true |
― |
未定義値との不等号比較 |
― |
― |
(null !== undefined) true (null != undefined) false |
― |
ポイントは3つです。
・VB.NET では、String 型の等価/不等価演算子は Nothing と空文字を等しいと判断します。
・JavaScript では、非厳密演算子は null と undefined を等しいと判断します。
・ANSI/ISO 準拠のSQLでは、比較演算子による NULL との比較結果は UNKNOWN となります。
(SQL Server で ANSI_NULLS オプションをOFFにすると違う結果になりますが、今後のバージョンでサポートされなくなることがアナウンスされています)
なお、LINQ to Entities の値比較における null の扱いには別途注意が必要です。
Entity Framework 6 で UseDatabaseNullSemantics プロパティ が導入され、既定(false)で (operand1 == operand2) が次のような複雑なSQL式に変換されるようになりました。
※operand1, operand2 がどちらも NULL となりうるケース、たとえば operand1 が NULL 許可列、operand2 が String 型変数の場合を想定します。
(((operand1 = operand2) AND (NOT (operand1 IS NULL OR operand2 IS NULL))) OR ((operand1 IS NULL) AND (operand2 IS NULL)))
どちらかが NULL の場合、ANSI 準拠の等価演算子 '=' では常に UNKNOWN が返されてしまうため、NULL を絡めた比較も正しく行われるような考慮がされています。
設定を true に変えれば (operand1 = operand2) という素直な式が生成されますが、NULL を正しく比較できないことを認識したうえで設定しないと「不可解な」挙動に頭を抱えることになりかねませんので注意しましょう。
■Windows フォーム
HawkEye 2
実行中のEXEにアタッチしてプロパティ、フィールドの確認/変更、イベントハンドラの確認/実行などができます。
あまり知られていませんが(日本語の紹介記事はほかに見たことがない)、すごいツールです。
元祖 HawkEye の時代から、10年以上の間重宝しています。
CodePlex で開発されていた HawkEye(.NET 3.5 まで)を引き継いだ形で、.NET 4 以降にも対応しています。
wfSpy
実行時にプロパティの確認/変更ができます。
ソースコードをダウンロードしてビルドします。
管理者として実行する必要があります。
■WPF
Visual Studio(2015 以降)
デバッグ中に [デバッグ]-[ウィンドウ] から「ライブビジュアルツリー」「ライブプロパティエクスプローラー」が使用できます。
WPF Inspector
非デバッグ実行時に XAML 要素をツリー表示し、プロパティやバインドされたビューモデルの中身を確認/変更できます。
リソース、トリガー、スタイルを確認することもできます。
■UIオートメーション
Windows フォーム、WPF、Windows ストアアプリ、UWP などのデスクトップアプリケーションにアクセスし、共通のインターフェイスでUI要素を扱うことができます。
Inspect
UI要素をツリーで確認できます。
UI Spy の後継という位置づけで、旧来の MSAAI (Microsoft Active Accessibility) にも対応しています(左上のドロップダウンで切り替え)。
ネイティブプログラムです。
マウスカーソルの位置にある要素がツリー上で自動選択され、属性が表示されます。
Windows Application Driver(WinAppDriver) を利用して自動UIテストを記述する際、要素のIDや名前、構成を確認するのに欠かせないツールです。
[Action] メニューから要素に対して操作を実行することも可能です。
Windows 用のソフトウェア開発キット (Windows SDK) に含まれています。
Windows SDK とエミュレーターのアーカイブ
%ProgramFiles(x86)%\Windows Kits\<major_version>\bin\<version>\<platform>\inspect.exe
※<> 内は環境によって異なります。
Visual UI Automation Verify (Visual UIA Verify)
UIオートメーションの実装を検証するUIテストライブラリのGUIドライバです。
UI要素をツリーで確認できます。
.NET アプリケーションです。
ツリーで選択された XAML 要素は実際のウィンドウ上でハイライト表示されます。
[Tests] ペインから要素に対する操作を検証することができます。
Windows 用のソフトウェア開発キット (Windows SDK) に含まれています。
Windows SDK とエミュレーターのアーカイブ
%ProgramFiles(x86)%\Windows Kits\<major_version>\bin\<version>\<platform>\UIAVerify\VisualUIAVerifyNative.exe
※<> 内は環境によって異なります。
WinAppDriver UI Recorder
記録がアクティブなときに行った操作が解析され、WinAppDriver を利用したテスト向けの C# コードが自動生成されます。
自動生成されたコードでは、要素は XPath の絶対パスで特定されます。
そのまま使えるほどメンテナンス性のよいコードが生成されるわけではありません。
Excel のマクロ記録と同様、補助的に使うのがよさそうです。
■ASP.NET
Glimpse
実行時にブラウザ下部に診断結果が表示されます。
コントロールツリー、サーバーイベント、各フェーズの処理時間、リクエスト、セッション、ルート解析など、様々な情報を確認することができます。
WebフォームでもMVCでも使用できます。
Prefix
ASP.NET Core に対応しています。
《セットアップ》
(1) Prefix をインストールします。
(2) StackifyMiddleware パッケージを Nuget でインストールします。
(3) Startup クラスの Configure メソッドで StackifyMiddleware をミドルウェア登録(UseMiddleware)します。
(4) タスクトレイの Prefix コンテキストメニューから [Enable .NET Profiler] を選択して有効化します。
(5) タスクバルーンで促されたら Visual Studio を再起動します。
(6) Webアプリケーションを IIS Express で開始します。
(7) タスクトレイの Prefix コンテキストメニューから [Open in browser] を選択し、トレースページを開きます。
Application Insights
監視データを Microsoft Azure に送信し、ポータルで診断結果を確認できます。
Azure アカウントが必要です。
料金や無料枠については こちら をご参照ください。
アプリケーションが Azure 上でホスティングされている必要はなく、「インストルメンテーション キー」で関連づけることにより、オンプレミス運用や開発環境のデバッグ目的でも使用できます。
監視適用方法には 「ビルド時(SDK)」「実行時(Status Monitor)」 の二種類があります。
オペレーティングシステム、IIS サーバー、サーバーアプリケーション、データベースアクセス、クライアントスクリプトが監視対象となり、リクエスト応答時間、内部処理のタイムライン、SQL、トレースログ、エラー内容、CPU/メモリ/ネットワーク使用率、ユーザー数とセッション数などが診断結果として確認できます。
カスタムクエリによる監視データ抽出、グラフ表示が可能な Analytics というツールも提供されています。
■ネットワーク
Fiddler
ブラウザ/非ブラウザの通信をキャプチャしてくれます。
リクエスト/レスポンスを上下に並べて、テキスト/JSON/XML/クッキーなど様々なビューで確認できます。
.NET 言語仕様の中でも、名前のわかりづらさと概念のややこしさが相まって、際立ってとっつきにくいのがこの「共変性」「反変性」ではないでしょうか。
どこで何を読んでも頭がこんがらがるばかりという方。
なんとなく使えているけど仕組みはよくわかっていないという方。
そのもやもやをいったんお引き受けします。
(お返しすることになったらごめんなさい)
一般的な解説とはあえて違った切り口で、かつ本質から逸れることなく、図を使わずに文章とコードのみで説明してみたいと思います。
互換性の概念であって状態遷移やデータフローの話ではありませんので、混乱を避けるため、(そもそも用語がわかりづらいという仮定で)語源を無視して、「変換」とか「変更」とか「→」といった表現はなるべく使わないようにしました。
初めにプログラミング言語における「共変性」「反変性」の概念を紐解いていきますが、その時点では理解がぼんやりでも問題ありません。
その後に掲載するコードと照らし合わせ、最終的に「そういうことか!」となっていただくのが目的です。
1. 現実世界の例
イメージを浮かべていただくため、たとえ話から入ります。
こういうのが苦手な方は飛ばしていただいてもかまいません。
―――――――――――――――――――――――――
現金会計の牛丼屋さんがいます。
肉にこだわりはありません。
ある日風邪をひいて寝込んでしまいました。
噂を聞いて、クレジットカードも使えるこだわりのチバザビーフ専門牛丼(以下「チバビ丼」)屋さんがやってきました。
「今日はうち定休日だから、代わりにやってあげるよ」
「ありがとう、助かるよ」
その日、チバビ丼屋さんが現金を受け取ってチバビ丼を出し、無事に代わりを務めることができました。
何日かして、今度はチバビ丼屋さんが風邪をひいてしまいました。
次は僕の番と、牛丼屋さんがやってきます。
「今日はうち定休日だから、代わりにやってあげるよ」
「ありがとう、助かるよ」
牛丼屋さんが代わりを務めることになりました。
早速お客さんがやってきます。
「カード使えますよね?」
「ああ、ごめんなさい。現金だけなんです」
「え? 前は使えたと思うんだけどな」
またお客さんがやってきます。
「チバビ丼、並!」
「ああ、ごめんなさい。今日は神戸産なんですよ」
「ええ? チバザビーフじゃないの?」
―――――――――――――――――――――――――
この話のポイントは「代わりが務まるか?」というところです。
(細かい設定の不備はご容赦ください)
牛丼屋さんの何がいけなかったのでしょうか?
お客さんはお店に期待をしています。
その期待に応えられれば、お客さんを引き継ぐことができます。
お客さんは牛丼屋に
・現金で支払えること
・牛丼を作ってもらうこと
を期待します。
チバビ丼屋さんはどちらの期待にも応えることができたので、代わりが務まりました。
一方、チバビ丼屋のお客さんはお店に
・現金だけでなくカードも使えること
・チバザビーフを使った牛丼を作ってもらうこと
を期待します。
牛丼屋さんはどちらの期待にも応えることができず、代わりが務まりませんでした。
特に理不尽なところはありませんね。
この話に納得できたなら、「共変性」「反変性」の原理を感覚的に理解しているということになります。
後はプログラムの世界で整理するだけです。
2. そもそも何についての話なのか?
型の入出力規格の互換性の話です。
ここで入出力規格とは、入力(主に引数)型、出力(主に戻り値)型の定義を指しています。
型自体に直接の継承関係がなくても、入出力規格に互換性があれば、代入を許可しても問題ないことがあります。
しかし、それを実現するためには言語による明示的なサポートが必要です。
どのような入力型、出力型で構成された関係なら代入が許可されるか。
それがメインテーマとなります。
―――――――――――
T t = (U)u;
この代入が可能か?
代入先(左辺)の型 T
└ 機能
├ 入力型 TIn
└ 出力型 TOut
↑ 代入したい
代入元(右辺)の型 U
└ 機能
├ 入力型 UIn
└ 出力型 UOut
―――――――――――
(1) 入力型、出力型それぞれの互換(上記 TIn-UIn, TOut-UOut) (2) 入出力機能を擁する型の互換(上記 T-U) という異なる二つのレベルの互換があります。
(1) によって (2) が決まる、という関係です。
これらを混同しないことがポイントです。
ここでの関心の中心は (2) の互換です。
入力型、出力型の互換性が、その機能を擁する型の互換性にどう影響するのか。
上のメインテーマはそう言い換えることもできます。
3. 前提となる言葉の定義
「変性」は、クラス階層(継承関係)における型の互換性に関する概念です。
基底型変数には派生型インスタンスを代入して扱うことができますが、このとき「代入互換性」があると言います。
これと同じ方向に、派生型に対して互換性を持つことを「共変」と言います。
反対の方向に、基底型に対して互換性を持つことを「反変」と言います。
どちらの互換もない場合は「不変」です(.NET では値型は不変です)。
4. 最も短い説明
共変性
派生型を返す処理も扱える。
(チバザビーフ専門でも牛丼屋を営業できる)
反変性
基底型を受け取る処理も扱える。
(金種を問わないお店も現金専門店を営業できる)
5. もう少し詳しく
共変性がサポートされると
「基底型を返す変数」に「派生型を返すオブジェクト」を代入できます。
使用する側は、基底型が返されることを想定していますが、その中身が派生型であっても不都合はないはずです。
(お客さんは牛丼を注文しますが、牛がチバザビーフでも不都合はないはずです)
反変性がサポートされると
「派生型を受け取る変数」に「基底型を受け取るオブジェクト」を代入できます。
使用する側は、派生型を渡しますが、それが基底型として扱われても不都合はないはずです。
(お客さんはクレジットカードを渡しますが、それが代金として扱われて不都合はないはずです)
6. 興味のある方はさらに進んで(ちょっと寄り道)
リスコフの置換原則(LSP:Liskov Substitution Principle)との関係
リスコフの置換原則では、基底型変数の中身を基底型オブジェクトから派生型オブジェクトに置き換えても、動作の妥当性が損なわれないことを保証すべきとしています。
そうすると使用する側は、中身の型を(基底型なのか、あるいはどの派生型なのかを)意識しなくてすみますね。
上記「もう少し詳しく」に記載しました共変性/反変性の「不都合はないはず」は、その継承関係においてリスコフの置換原則が守られていることが前提となります。
リスコフの置換原則に準拠するためのルールには、契約(コントラクト)に関するルールと変性に関するルールがあります。(※1)
[契約に関するルール]
・メソッド開始時に成立しているべき事前条件(通常は引数)を派生型で強化(厳しく)することはできない。
つまり、基底メソッドの条件を満たす引数は、派生型オーバーライドでも通す必要がある。
・メソッド終了時に成立しているべき事後条件(プロパティや戻り値など)を派生型で緩めることはできない。
つまり、派生型オーバーライドの戻り値は、最低限、基底メソッドの戻り値条件を満たす必要がある。
・基底型の不変条件(常に満たしているべき条件)は派生型でも維持されなければならない。
※このような契約を検証するためのツールとして、.NET には「コードコントラクト」という仕組みが用意されています。
(開発は停滞しているようですので、新たな導入には慎重になった方がよさそうです)
[変性に関するルール]
派生型メソッドの引数型には反変性、戻り値型には共変性がなくてはならない。
※.NET Framework の CLR (Common Language Infrastructure) では、ジェネリック型とデリゲートでこの概念が使われています。
オーバーライドメソッドで引数型や戻り値型を変えることはできないため、型の継承関係においてはもとより適用されません。
(new 修飾子によって隠ぺいすると別の型で再定義できますが、基底型変数からは呼ばれませんので、ここでの議論からは外れます)
いかがでしょう。
おおよその概念はつかんでいただけたでしょうか。
上に書いたとおり、この時点ではぼんやりの理解でも心配いりません。
たいへんお待たせしました。
いよいよコードを見ていきましょう。
.NET における「共変性」「反変性」の例をあげていきます。
上述の概念と照らし合わせてご覧ください。
■配列
― 配列の共変性 ―
基底型配列変数(基底型を返す)に、派生型配列(派生型を返す)を代入できる。
// 参照型 B を参照型 A に代入できるなら、配列 B[] も A[] に代入できる。
string[] strings = new string[] { "a", "b" };
object[] objects = strings;
// 代入後の配列インスタンスは同一参照
Assert.IsTrue(object.ReferenceEquals(objects, strings));
// ただし、要素に別の派生型の値を設定しようとすると実行時例外が発生する。
// 配列には値を返すだけでなく受け取る役目もあるのに、
// 丸ごと(本来適用すべきでない受け取り操作を含めて)共変を許してしまったことの弊害である。
try
{
objects[1] = 1;
Assert.Fail();
}
catch (ArrayTypeMismatchException ex)
{
// 「配列と互換性のない型の要素にアクセスしようとしました」
Console.WriteLine(ex.ToString());
}
// 配列の共変性は、不変である値型には適用されない。
/* コンパイルエラー「型 'int[]' を型 'object[]' に暗黙的に変換できません。」
objects = new int[] { 0, 1 };
*/
■デリゲート
― デリゲートの共変性(.NET 2.0 ~) ―
基底型を返すデリゲート変数に、派生型を返すオブジェクトを代入できる。
public delegate Base BaseGetter();
public Base GetBase()
{
return new Base();
}
public Derived GetDerived()
{
return new Derived();
}
public void SupportCovariance()
{
BaseGetter returnsBase = this.GetBase;
// 共変性のサポートがこの代入を可能にする。
BaseGetter returnsDerived = this.GetDerived;
}
― デリゲートの反変性(.NET 2.0 ~) ―
派生型を受け取るデリゲート変数に、基底型を受け取るオブジェクトを代入できる。
// EventArgs 型の引数を受け取るイベントハンドラ
private void EventArgsHandler(object sender, EventArgs e)
{
// :
}
public Contravariance()
{
// KeyDown が期待するイベント引数型は KeyEventArgs だが、
// 反変性のサポートによって EventArgs 型引数のイベントハンドラも登録できる。
this.KeyDown += this.EventArgsHandler;
}
■ジェネリック
― ジェネリックの共変性(.NET 4 ~) ―
基底型を戻り値型とする変数に、派生型を戻り値型とするオブジェクトを代入できる。
// これは .NET 3.5 でもできた。
IEnumerable<Derived> derivedEnumerable = new List<Derived>();
// IEnumerable<out T> のように共変であることを表す out キーワードで修飾されるようになった。
// 列挙だけなら Derived 型オブジェクトを Base 型として扱っても問題ない。
IEnumerable<Base> baseEnumerable = derivedEnumerable;
// 設定もできる場合、共変は許されない。
// (List<Base> は Derived 以外の派生型オブジェクトを受け取ることもあるから)
/* コンパイルエラー「変換できません」
List<Base> baseList = new List<Derived>();
List<Base> baseList = (List<Base>)(new List<Derived>());
*/
― ジェネリックの反変性(.NET 4 ~) ―
派生型を引数型とする変数に、基底型を引数型とするオブジェクトを代入できる。
Action<Base> baseAction = (target) => { target.DoSomething(); };
// Action<in T> のように、反変であることを表す in キーワードで修飾されるようになった。
// 基底型の引数に派生型のオブジェクトが渡されても問題ない。
Action<Derived> derivedAction = baseAction;
// 引数を渡す側は、派生型を渡してそれが基底型として扱われても不都合はないはず。
derivedAction.Invoke(new Derived());
---
もやもや、少し晴れたでしょうか。
初めに大きなことを言ってしまいましたが、一度読んだだけですぐにはすっきりしないと思います。
(余計に混乱した、という方がいらしたら申し訳ありません)
一つでもピンとくる表現があったなら、書いた意味がありました。
またもやもやしたら戻ってきてください。
---
※1 《参考文献》Gary McLean Hall (著), 長沢 智治(監訳) (その他), クイープ (翻訳)『C#実践開発手法』日経BP社
---
《付録》
わかりやすく命名し直してみました。
ぜひ置き換えて読み直してみてください。
共変性 → 出力互換性
反変性 → 入力互換性
.NET では、コンストラクタから仮想メソッドを呼び出すと、派生型コンストラクタが処理されていない状態でオーバーライドメソッドが実行されてしまいます。
例として、基底クラス Base と 派生クラス Derived があるとします。
(括弧付き数字は実行される順番です)
// 基底クラス
class Base
{
private static bool baseStaticFieldInitialized = ConsoleOut("(1) 基底型静的フィールドの初期化");
private bool baseInstanceFieldInitialized = ConsoleOut("(4) 基底型インスタンスフィールドの初期化");
public Base()
{
ConsoleOut("(5) 基底型コンストラクタの実行");
// ※派生型コンストラクタはまだ実行されていないが、この時点でもインスタンスは派生型
Assert.AreEqual(typeof(Derived), this.GetType());
// ※オーバーライドされた派生型メソッドが実行される。
OverridableMethod();
}
// 仮想メソッド
protected virtual void OverridableMethod()
{
// ※インスタンス型でオーバーライドされている場合、コンストラクタからの呼び出しでも実行されない。
Assert.Fail();
}
protected static bool ConsoleOut(string step)
{
Console.WriteLine(step);
return true;
}
}
// 派生クラス
sealed class Derived : Base
{
private static bool derivedStaticFieldInitialized = ConsoleOut("(2) 派生型静的フィールドの初期化");
// ※基底型インスタンスフィールド、基底型コンストラクタより先に処理される。
private bool derivedInstanceFieldInitialized = ConsoleOut("(3) 派生型インスタンスフィールドの初期化");
private bool derivedConstructorCalled = false;
public Derived()
{
ConsoleOut("(7) 派生型コンストラクタの実行");
this.derivedConstructorCalled = true;
}
// オーバーライドメソッド
protected override void OverridableMethod()
{
ConsoleOut("(6) 派生型オーバーライドメソッドの実行");
// ※基底型コンストラクタから呼び出された場合、
// 派生型フィールドは初期化済みだが、派生型コンストラクタが未処理のまま実行されてしまう。
Assert.IsTrue(this.derivedInstanceFieldInitialized);
Assert.IsFalse(this.derivedConstructorCalled);
}
}
実行結果は以下のようになります。
new Derived();
↓
(1) 基底型静的フィールドの初期化
(2) 派生型静的フィールドの初期化
(3) 派生型インスタンスフィールドの初期化
(4) 基底型インスタンスフィールドの初期化
(5) 基底型コンストラクタの実行
(6) 派生型オーバーライドメソッドの実行
(7) 派生型コンストラクタの実行
なお、非 sealed 型コンストラクタからの仮想メソッドを呼び出しは、コード分析(FxCop)で、「CA2214: コンストラクターのオーバーライド可能なメソッドを呼び出しません。」として警告されます。
単体テストでモックライブラリ「Moq」を利用すると、モックオブジェクトを動的に作成することができてとても便利ですね。
ここでは基本的な使い方は割愛させていただき、応用的な使い方をご紹介します。
(アーキテクチャ上/プラクティスとしての是非についてはここでは触れません。)
まずは internal(Friend)メンバの Setup からです。
既定では、Moq が Setup できるのは public な仮想メンバのみです。
テスト対象プロジェクトの AssemblyInfo.cs に
[assembly: InternalsVisibleTo("FooTest")]
のような宣言を記述することで、テストメソッドから internal メンバにアクセスすることができますが、それだけでは十分ではありません。
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
の宣言を追加することで、Moq の Setup が効くようになります。
protected メンバも Setup することが可能です。
using ディレクティブで Moq.Protected 名前空間を指定しておくと、
mock.Protected().Setup<bool>("ProtectedMethod", "引数").Returns(true);
のようにして、リフレクションベースではありますが、モック化することができます。
次にご紹介するのは、部分的な Setup の方法です。
Mock インスタンスの CallBase プロパティに true を設定すると、明示的に Setup していない仮想メンバについて、基底クラスの動作を保持することができます。
実例を見て見ましょう。
以下のようなテスト対象クラスがあるとします。
public class SampleClass
{
public virtual bool PublicVirtualProperty
{
get { return true; }
}
public virtual bool PublicVirtualMethod()
{
return true;
}
public virtual void WriteArg( string arg )
{
Console.WriteLine("WriteArg が引数 \"{0}\" で呼ばれました。", arg);
}
}
既定の Mock インスタンスでテストすると次のようになります。
// モックオブジェクト
var mock = new Mock<SampleClass>();
SampleClass sample = mock.Object;
// プロパティ動作を上書き
mock.SetupGet(s => s.PublicVirtualProperty).Returns(false);
// 検証
Assert.IsFalse(sample.PublicVirtualProperty);
Assert.IsFalse(sample.PublicVirtualMethod()); // ★明示的に Setup していないが、既定で動作は上書きされており、false が返る。
mock.VerifyAll();
PublicVirtualMethod の動作を基底クラス SampleClass のまま(true を返す)にしたい場合は、以下のようにします。
mock.CallBase = true; // ★Setup で定義した呼び出し以外は基底クラスの動作とする。
Assert.IsFalse(sample.PublicVirtualProperty); // Setup したメソッドの動作は上書きされたまま。
Assert.IsTrue(sample.PublicVirtualMethod()); // ★基底クラスの仮想メソッドが呼ばれ、true が返る。
この CallBase の動作を前提に、メソッド呼び出し時の引数の値を動的に書き換えてみます。
(実用には向かないと思いますので、余興としてご覧ください)
// モックオブジェクト
var mock = new Mock<SampleClass>();
mock.CallBase = true; // Setup で定義した呼び出し以外は基底クラスの動作を保持
SampleClass sample = mock.Object;
// 書き換え後の引数値
string hackArg = "hackArg";
// 引数を強制的に hackArg にして実行する。
mock.Setup(s => s.WriteArg(It.Is<string>(arg => arg != hackArg)))
// It.Is に該当する(引数が hackArg でない)と基底クラスのメソッドが引数 hackArg で呼び出される。
.Callback<string>((arg) => sample.WriteArg(hackArg));
// 実行
sample.WriteArg("originalArg");
// → 「WriteArg が引数 "hackArg" で呼ばれました。」
// 検証
mock.VerifyAll();
ストレートな方法でないのが少し残念です。
上の例は単純化して WriteArg を直接実行していますが、間接的に呼び出されるメソッドの引数値をテスト用に変える、といったことが可能になります。
モックライブラリ Moq とDIコンテナ Unity(※1)を使用してモック化する例です。
単体テストの基底クラスにモックの生成、検証を抽出することで、Verify 漏れを防ぎ、素早く、すっきりテストコードを書くことができます。
※1 ゲームエンジンの方ではありません。Microsoft patterns & practices のプロジェクトとして開発された軽量DIコンテナで、現在はその手を離れ、オープンソースで運営されています。
■プロダクションコード
【Unity のヘルパー】
クライアントクラスのコンストラクタから使用します。
共用するDIコンテナを保持しています。
internal static class UnityHelper
{
/// <summary>
/// シングルトンインスタンス向けのコンテナ。
/// </summary>
public static IUnityContainer SingletonContainer { get; } = CreateSingletonContainer();
/// <summary>
/// プロパティインジェクションを自動化したコンテナを取得する。
/// </summary>
public static IUnityContainer GetPropertInjectionContaine<T>(T instance)
{
SingletonContainer.BuildUp<T>(instance);
return SingletonContainer;
}
/// <summary>
/// シングルトンインスタンス向けの親コンテナを引き継いだ子コンテナを作成する。
/// </summary>
/// <remark>インスタンスは子コンテナ内でシングルトンとなる(別コンテナでは別インスタンス)。</remarks>
public static IUnityContainer CreateSingletonBasedContainer()
{
return SingletonContainer.CreateChildContainer();
}
/// <summary>
/// シングルトンインスタンス向けのコンテナを作成する。
/// </summary>
private static IUnityContainer CreateSingletonContainer()
{
var container = new UnityContainer();
// アセンブリ内の全具象クラスをシングルトンインスタンスで登録する。
// ※必要に応じて絞り込んだり、個別に登録したりする。
// ※public コンストラクタが複数あると引数の多い方が選択されてしまうようなので、回避する場合は個別に Register する。
container.RegisterTypes(
AllClasses.FromLoadedAssemblies(),
WithMappings.FromMatchingInterface,
WithName.Default,
t => new ContainerControlledLifetimeManager(),
overwriteExistingMappings: true);
return container;
}
}
【クライアントクラス】(System Under Test)
public class Client
{
/// <summary>
/// DIコンテナ。
/// </summary>
internal IUnityContainer DiContainer { get; set; }
/// <summary>
/// コンストラクタ。
/// </summary>
public Client()
{
// プロパティインジェクションが自動化されたコンテナを取得
this.DiContainer = UnityHelper.GetPropertInjectionContainer(this);
/* インスタンス生成のタイミング等を変えたい場合は子コンテナで構成する。
this.DiContainer = UnityHelper.CreateSingletonBasedContainer();
// テスト対象オブジェクト内で一つのインスタンスを共用する場合
this.DiContainer.RegisterInstance<Service>(new Service());
// コンポーネントを毎回デフォルトコンストラクタで生成する場合
this.DiContainer.RegisterType<Service>(new TransientLifetimeManager(), new InjectionConstructor());
// 毎回の生成ロジックを調整する場合
this.DiContainer.RegisterType<Service>(
new InjectionFactory(c => {
return new Service("Production_InjectionFactory");
}));
*/
}
/// <summary>
/// 依存サービス。
/// </summary>
[Dependency]
protected Service Service { get; set; }
/// <summary>
/// テスト対象メソッド。
/// </summary>
public string Act()
{
// プロパティ経由でDIコンテナから依存オブジェクトを取得して使用する。
return this.Service.GetText();
}
}
【依存サービス】(Depended-On Component)
public class Service
{
private readonly string text = "Production";
public Service()
{
}
internal Service(string text)
{
this.text = text;
}
public virtual string GetText()
{
return this.text;
}
}
■単体テスト
【テストの基底クラス】
DIコンテナの管理、モックオブジェクトの生成、管理、検証(Verify)を担います。
ほかのDIコンテナに切り替えるときは、個々のテストコードは変えずに基底クラスを差し替えます。
(MSTest 例)
public abstract class UnityTestBase
{
/// <summary>
/// モックオブジェクトのリスト。
/// </summary>
private List<Mock> mocks;
/// <summary>
/// 単体テスト用にモックオブジェクトを提供するDIコンテナ。
/// </summary>
private IUnityContainer diContainer;
/// <summary>
/// テストの初期処理。
/// </summary>
[TestInitialize]
public void BaseTestInitialize()
{
this.diContainer = new UnityContainer();
this.mocks = new List<Mock>();
}
/// <summary>
/// テストの終了処理。
/// </summary>
[TestCleanup]
public void BaseTestCleanup()
{
// モックが期待どおりに呼び出されたことを検証する。
foreach (var mock in this.mocks)
{
mock.VerifyAll();
}
this.diContainer.Dispose();
}
/// <summary>
/// Mock インスタンスを生成する。
/// </summary>
protected Mock<T> CreateMock<T>()
where T : class
{
return CreateMock<T>(false);
}
/// <summary>
/// Mock インスタンスを生成する。
/// </summary>
protected Mock<T> CreateMock<T>(bool callBase)
where T : class
{
var mock = new Mock<T> { CallBase = callBase };
this.diContainer.RegisterInstance(mock.Object);
this.mocks.Add(mock);
return mock;
}
/// <summary>
/// テスト用のDIコンテナを取得する。
/// </summary>
protected IUnityContainer GetDiContainer<T>(T instance)
{
this.diContainer.BuildUp<T>(instance);
return this.diContainer;
}
}
【テストクラス】
モックをDIコンテナに詰めて渡すので、依存コンポーネントをモックに差し替えるためだけに、Client を継承した Testable クラスを用意する必要はありません。
モックインスタンスは、個々のテストメソッドに必要なものだけ生成します。
(MSTest 例)
[TestClass]
public class ClientTest : UnityTestBase
{
[TestMethod]
public void Act_Mocking()
{
// モック設定
var serviceMock = CreateMock<Service>();
serviceMock.Setup(s => s.GetText()).Returns("UnitTest");
// テスト対象オブジェクトを生成(モック用のDIコンテナを設定)
var client = new Client();
client.DiContainer = GetDiContainer(client);
// テスト対象メソッドを実行
string result = client.Act();
// 戻り値の検証
// ※モックの検証は基底クラスで自動的に行われる。
Assert.AreEqual("UnitTest", result);
}
}
モックライブラリ Moq とIoCコンテナ Autofac を使用してモック化する例です。
単体テストの基底クラスにモックの生成、検証を抽出することで、Verify 漏れを防ぎ、素早く、すっきりテストコードを書くことができます。
■プロダクションコード
【Autofac のヘルパー】
クライアントクラスのコンストラクタから使用します。
共用するDIコンテナを保持しています。
internal static class AutofacHelper
{
/// <summary>
/// protected set アクセサを持つプロパティのセレクタ。
/// </summary>
public static readonly IPropertySelector ProtectedSetterSelector =
new DelegatePropertySelector((p, o) => p.CanWrite && (p.SetMethod?.IsFamily ?? false));
/// <summary>
/// シングルトンインスタンス向けのコンテナ。
/// </summary>
public static IContainer SingletonContainer { get; } = CreateSingletonContainer();
/// <summary>
/// プロパティインジェクションを自動化したコンテナを取得する。
/// </summary>
public static IContainer GetPropertInjectionContainer(object instance)
{
SingletonContainer.InjectProperties(instance, ProtectedSetterSelector);
return SingletonContainer;
}
/// <summary>
/// シングルトンインスタンス向けのコンテナを作成する。
/// </summary>
private static IContainer CreateSingletonContainer()
{
var containerBuilder = new ContainerBuilder();
// アセンブリ内の全具象クラスをシングルトンインスタンスで登録する。
// ※必要に応じて絞り込んだり、個別に登録したりする。
// ここでは DependencyResolutionException 対策として Moq のインターフェイスプロキシを除外。
containerBuilder.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies())
.Where(t => t.Namespace != "Castle.Proxies")
.AsImplementedInterfaces()
.AsSelf()
.UsingConstructor(new DefaultOrFewestConstructorSelector())
.SingleInstance();
return containerBuilder.Build();
}
}
/// <summary>
/// 引数なしか最も少ないコンストラクタを優先する選択クラス。
/// </summary>
public class DefaultOrFewestConstructorSelector : IConstructorSelector
{
public ConstructorParameterBinding SelectConstructorBinding(ConstructorParameterBinding[] constructorBindings)
{
return constructorBindings.OrderBy(b => b.TargetConstructor.GetParameters().Length).FirstOrDefault();
}
}
【クライアントクラス】(System Under Test)
public class Client
{
/// <summary>
/// DIコンテナ。
/// </summary>
internal IContainer DiContainer { get; set; }
/// <summary>
/// コンストラクタ。
/// </summary>
public Client()
{
// プロパティインジェクションが自動化されたコンテナを取得
// ※Resolve のたびにインスタンスを生成、オーナーオブジェクト内で一つのインスタンスを共用したいなど、
// インスタンス生成のタイミング等を変えたい場合は独自に ContainerBuilder で構成する。
this.DiContainer = AutofacHelper.GetPropertInjectionContainer(this);
}
/// <summary>
/// 依存サービス。
/// </summary>
protected Service Service { get; set; }
/// <summary>
/// テスト対象メソッド。
/// </summary>
public string GetText()
{
// プロパティ経由でDIコンテナから依存オブジェクトを取得して使用する。
return this.Service.GetText();
}
}
【依存サービス】(Depended-On Component)
public class Service
{
private readonly string text = "Production";
public Service()
{
}
public Service(string text)
{
this.text = text;
}
public virtual string GetText()
{
return this.text;
}
}
■単体テスト
【テストの基底クラス】
DIコンテナの管理、モックオブジェクトの生成、管理、検証(Verify)を担います。
ほかのDIコンテナに切り替えるときは、個々のテストコードは変えずに基底クラスを差し替えます。
(MSTest 例)
/// <summary>
/// 単体テストの基底クラス。
/// </summary>
public abstract class AutofacTestBase
{
/// <summary>
/// モックオブジェクトのリスト。
/// </summary>
private List<Mock> mocks;
/// <summary>
/// 単体テスト用にモックオブジェクトを提供するDIコンテナのビルダー。
/// </summary>
internal ContainerBuilder DiContainerBuilder { get; private set; }
/// <summary>
/// テストの初期処理。
/// </summary>
[TestInitialize]
public void BaseTestInitialize()
{
this.DiContainerBuilder = new ContainerBuilder();
this.mocks = new List<Mock>();
}
/// <summary>
/// テストの終了処理。
/// </summary>
[TestCleanup]
public void BaseTestCleanup()
{
// モックが期待どおりに呼び出されたことを検証する。
foreach (var mock in this.mocks)
{
mock.VerifyAll();
}
}
/// <summary>
/// Mock インスタンスを生成する。
/// </summary>
protected Mock<T> CreateMock<T>()
where T : class
{
return CreateMock<T>(false);
}
/// <summary>
/// Mock インスタンスを生成する。
/// </summary>
protected Mock<T> CreateMock<T>(bool callBase)
where T : class
{
var mock = new Mock<T> { CallBase = callBase };
this.DiContainerBuilder.RegisterInstance(mock.Object);
this.mocks.Add(mock);
return mock;
}
/// <summary>
/// テスト用のDIコンテナを取得する。
/// </summary>
protected IContainer GetDiContainer(object instance)
{
var container = this.DiContainerBuilder.Build();
container.InjectProperties(instance, AutofacHelper.ProtectedSetterSelector);
return container;
}
}
▼【テストクラス】
モックをDIコンテナに詰めて渡すので、依存コンポーネントをモックに差し替えるためだけに、Client を継承した Testable クラスを用意する必要はありません。
モックインスタンスは、個々のテストメソッドに必要なものだけ生成します。
(MSTest 例)
[TestClass]
public class ClientTest : AutofacTestBase
{
[TestMethod]
public void Actt_Mocking()
{
// モック設定
var serviceMock = CreateMock<Service>();
serviceMock.Setup(s => s.GetText()).Returns("UnitTest");
// テスト対象オブジェクトを生成(モック用のDIコンテナを設定)
var client = new Client();
client.DiContainer = GetDiContainer(client);
// テスト対象メソッドを実行
string result = client.Act();
// 戻り値の検証
// ※モックの検証は基底クラスで自動的に行われる。
Assert.AreEqual("UnitTest", result);
}
}
よく使いそうなリフレクションのスニペットを集めてみました。
※型を明示するため、var はあえて使用していません(一部除く)。
※安全性と処理速度を考慮し、利用は必要最低限に絞りましょう。
// ▼インスタンス生成
{ // public/引数なし
Sample sample = Activator.CreateInstance<Sample>();
}
{ // public/引数あり
object[] args = new object[] { true };
Sample sample = (Sample)Activator.CreateInstance(typeof(Sample), args);
}
{ // 非 public/引数あり
object[] args = new object[] { 1 };
Sample sample = (Sample)Activator.CreateInstance(typeof(Sample), BindingFlags.NonPublic | BindingFlags.Instance, null, args, null);
}
// ▼フィールドアクセス
{ // 非 public/インスタンスフィールド(静的フィールドの場合、BindingFlags.Instance → BindingFlags.Static)
Sample sample = new Sample();
FieldInfo field = sample.GetType().GetField("privateField", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(sample, true);
bool fieldValue = (bool)field.GetValue(sample);
}
// ▼プロパティアクセス
{ // 非 public/インスタンスプロパティ(静的プロパティの場合、BindingFlags.Instance → BindingFlags.Static)
Sample sample = new Sample();
PropertyInfo property = sample.GetType().GetProperty("PrivateProperty", BindingFlags.NonPublic | BindingFlags.Instance);
property.SetValue(sample, true, null);
bool propertyValue = (bool)property.GetValue(sample, null);
}
// ▼メソッド呼び出し
{ // 非 public/インスタンスメソッド
Sample sample = new Sample();
MethodInfo method = sample.GetType().GetMethod("PrivateMethod", BindingFlags.NonPublic | BindingFlags.Instance);
object[] args = new object[] { 1 };
bool returnValue = (bool)method.Invoke(sample, args);
}
{ // ref 引数
// 引数を false から true に変える ChangeByRefToTrue メソッドを呼び出す。
Sample sample = new Sample();
Type[] paramTypes = new Type[] { typeof(bool).MakeByRefType() };
MethodInfo method = sample.GetType().GetMethod("ChangeByRefToTrue", BindingFlags.NonPublic | BindingFlags.Instance, null, paramTypes, null);
bool inValue = false;
object[] args = new object[] { inValue };
method.Invoke(sample, args);
bool outValue = (bool)args[0];
Assert.IsFalse(inValue);
Assert.IsTrue(outValue);
}
// ▼属性情報
{
SampleAttribute attribute = (SampleAttribute)Attribute.GetCustomAttribute(typeof(Sample), typeof(SampleAttribute));
bool AttributeProperty = attribute.Foo;
}
// ▼アセンブリ情報
{
// 現在実行中のコードを格納しているアセンブリ
Assembly executingAssembly = Assembly.GetExecutingAssembly();
// 現在実行中のメソッドを呼び出したメソッドのアセンブリ
Assembly callingAssembly = Assembly.GetCallingAssembly();
// 実行可能ファイルのアセンブリ(アンマネージアプリケーションから読み込まれている場合、null)
Assembly entryAssembly = Assembly.GetEntryAssembly();
// 実行可能ファイルのパス
string exePath = Application.ExecutablePath;
}
// ▼型情報
{ // 判定
Type type = typeof(Sample);
// 列挙型か。
bool isEnum = type.IsEnum;
// 値型か。
bool isValueType = type.IsValueType;
// Nullを許可する値型か。
bool isNullableValueType = (Nullable.GetUnderlyingType(type) != null);
}
{ // 型名
Assert.AreEqual("String", typeof(String).Name);
// 型名(コンパイル時解決)
// ※C# 6.0 / VB 2015~ ならリフレクションよりまずこちらを検討
Assert.AreEqual("String", nameof(String));
}
// ▼メソッド情報
{ // 現在のメソッド名
string currentMethodName = MethodBase.GetCurrentMethod().Name;
}
{ // 呼び出し元のメソッド名
// ※コンパイラによってインライン展開されていないことが前提です。
// ※.NET 4.5~ は、CallerMemberNameAttribute を使用して引数から取得できます。
StackFrame frame = new StackFrame(1);
string callerMethodName = frame.GetMethod().Name;
}
{ // メソッド名(コンパイル時解決)
// ※C# 6.0 / VB 2015~ ならリフレクションよりまずこちらを検討
Assert.AreEqual("IndexOf", nameof(String.IndexOf));
}
// ▼プロパティ情報
// 現在のプロパティ名
public bool Property
{
get
{
MethodBase accessor = MethodBase.GetCurrentMethod();
string propertyName = GetProperty(accessor).Name;
return true;
}
}
// アクセサからプロパティ情報を取得するメソッド
public static PropertyInfo GetProperty(MethodBase accessor)
{
PropertyInfo[] properties = accessor.DeclaringType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
foreach (PropertyInfo property in properties)
{
MethodInfo[] methods = property.GetAccessors(true);
foreach (MethodInfo method in methods)
{
if (method == accessor)
{
return property;
}
}
}
return null;
}
// Lambda 式からプロパティ名を取得するメソッド
// 使用例:GetPropertyName((Sample s) => s.PublicProperty)
public static string GetPropertyName<TObject, TProperty>(Expression<Func<TObject, TProperty>> getter)
{
var lambda = (LambdaExpression)getter;
var memberExpression = lambda.Body as MemberExpression;
if (memberExpression == null)
{
// TProperty が object の場合、ボックス化によって UnaryExpression となる。
var body = (UnaryExpression)lambda.Body;
memberExpression = (MemberExpression)body.Operand;
}
return memberExpression.Member.Name;
}
{ // プロパティ名(コンパイル時解決)
// ※C# 6.0 / VB 2015~ ならリフレクションよりまずこちらを検討
Assert.AreEqual("Length", nameof(String.Length));
}
税込価格から本体価格を算出する方法は、消費税の端数処理方式によって異なります。
たとえば税率8%で税込価格が110円の場合、切り上げ方式で端数処理した消費税なら本体価格は101円ですが、切り捨て方式で端数処理した消費税なら本体価格は102円です。
以下、端数処理方式別の算出式です。
※rate には、0.05m, 0.08m, 0.10m などの消費税率(正の値)が入ります。
▼四捨五入方式で端数処理した税込価格 → 本体価格
// C#
Math.Ceiling((priceWithTax - 0.5m) / (1m + rate))
' VB.NET
Math.Ceiling((priceWithTax - 0.5D) / (1D + rate))
▼切り捨て方式で端数処理した税込価格 → 本体価格
// C#
Math.Ceiling(Math.Abs(priceWithTax) / (1m + rate)) * (priceWithTax >= 0 ? 1 : -1)
' VB.NET
Math.Ceiling(Math.Abs(priceWithTax) / (1D + rate)) * If(priceWithTax >= 0, 1, -1)
▼切り上げ方式で端数処理した税込価格 → 本体価格
// C#
Math.Truncate(priceWithTax / (1m + rate))
' VB.NET
Math.Truncate(priceWithTax / (1D + rate))
▼.NET Framework
『Essential .NET ― 共通言語ランタイムの本質』(Don Box, Chris Sells 著)
.NET の仕組みについて突っ込んだ解説がされています。
2001年頃、.NET 1.0 を対象に書かれた本ですが、今も変わらないコアな技術の話で構成されています。
▼プラクティス
『.NETのクラスライブラリ設計』(Krzysztof Cwalina, Bard Abrams 著)
「良い設計のフレームワーク」を作成するためのガイドライン、プラクティス、アンチパターンをまとめたものです。
個々のガイドラインに対し、Jeffrey Richer, Chris Anderson, Chris Sells ら多数の寄稿者による注釈が付記されています。
重点が少し異なるところもありますが、基本的にはアプリケーション開発者にとっても役立つ汎用的な内容です。
Visual Studio のコード分析機能(参考:コード標準化支援)に実装されているものも多くあります。
『Adaptive Code ~ C#実践開発手法 第2版』(Gary McLean Hall 著)
「アジャイル開発のフレームワーク」「アダプティブコードの基礎」「SOLIDコード」「アダプティブコードの適用」の四部構成で、「適応力のあるコード」を目指す本です。
オブジェクト指向設計の SOLID 原則について書かれた部分は、アジャイルに関係なくお勧めです。
デザインパターンや .NET の言語サポートと絡め、各原則の実用的な意義がサンプルコード付きでわかりやすく解説されています。
▼WPF
『エッセンシャル WPF』(Chris Anderson 著)
WPFチームのチーフアーキテクトがWPFの基本概念について解説したものです。
WPFの心がわかります(WPFが目指したことの理解に役立ちます)。
『プログラミング Windows 第6版 上』(Charles Petzold 著)
副題に「C#とXAMLによるWindowsストアアプリ開発」とあるとおり、ストアアプリを対象としていますが、多くの部分はWPFにも(一部読み替えて)適用できます。
(参考:プログラミング Windows 第6版 WPF編)
そこそこの分量(720ページ)があって章立ても網羅的なことから、辞書的に使おうとお考えになるかもしれませんが、通読に向いています。
MVVM についても一章割かれています。
▼ASP.NET MVC
『プログラミング Microsoft ASP.NET MVC』(Dino Esposito 著)
APIの解説から具体的な実装手法、アーキテクチャまで。ユニットテストにも一章割かれています。
▼Entity Framework
『Programming Entity Framework』(Julia Lerman 著)
英語でかなりの分量がありますが、日本語の本がない現状では、頑張って読む価値があります。
あまり知られていないソリューションがコード付きで紹介されていたりするので、興味のあるトピックを拾い読みするだけでも役に立つと思います。
ただし、最終第2版が2010年の刊行(CodeFirst/DbContext が導入されたEF4.1は第3刷でようやく触れる程度)と、結構年数が経っています。
新しい機能は他書や技術サイトで補ってください。
▼非同期処理
『C#によるマルチコアのための非同期/並列処理プログラミング』(山本 康彦 著)
.NET 1.x から .NET 4.5 までの非同期処理技術の進化を、豊富なサンプルコードとともにわかりやすく解説しています。
ThreadPool~APM(Asynchronous Programming Model)~EAP(Event-based Asynchronous Pattern)~TAP(Task-based Asynchronous Pattern)~async/await
▼ユニットテスト
『The Art of Unit Testing: with examples in C#』(Roy Osherove 著)
依存性の切り離し方、モックライブラリの使い方から、テストクラスのパターン/アンチパターンまで。
「モックとスタブの違いは?」といったトピックもあります。
▼番外編
『ADO 集中講座』(Rob Macdonald 著)
ADO.NET ではなく、ADO の本です。
(出番はかなり少なくなりましたが、VBA や古いアプリケーションの保守など、まだまだ活躍する機会はあるでしょう。)
Redordset の高速な操作、階層化... ADO を使い尽くすことができます。
『標準 FindBugs 完全解説』(宇野るいも, arton 著)
Java 用のバグ発見(静的解析)ツールのルール解説書です。
各ルールについて、なぜそれが潜在的なバグなのか、丁寧に説明されており、.NET プログラマにも得るところがあります。
よいプラクティスの生まれる一つの源になるかもしれません。
誰よりもバグが嫌いな方にお勧めです。
『オフショア開発に失敗する本』(幸地 司 著)
文化、常識の異なる人たちと一つの物を作り上げるときの心構えが学べます。
事例も教科書的でなく現場に即したもので、発注側/受注側双方の立場、気持ちの理解に役立ちます。
2008年刊行のため、業界の動向に関する記載に古い点もありますが、
オフショア開発はもちろんのこと、国内のアウトソーシングや国際交流一般にも応用できる内容です。
▼明示的な変換(キャスト)
キャスト演算子
C# のキャスト演算子 () に相当するものは VB.NET にはない。
DirectCast 演算子
・Object 型とほかの型との変換において、VBランタイムヘルパーを使用しない分、CType 関数より高速に動作する。
・値型と Object 型の間の変換にはボックス化、ボックス化解除が使われる。異なる型へはボックス化解除できず、暗黙の型互換があっても InvalidCastException が発生する。
・実行時の型が変更後の型と一致しないとキャストに失敗する。たとえば、Integer 型から String 型へは変換できない。
・C# のキャスト演算子 () と異なり、暗黙の型互換があっても変換できない。たとえば、Integer 型から Long 型への変換はコンパイルエラーとなる。
TryCast 演算子
・参照型のみ。
・C# の as 演算子に相当。IL の isinst 命令にコンパイルされる。
・TypeOf ... Is ... で判定してから DirectCast するのは変換が二度生じて無駄である。TryCast してから IsNot Nothing 判定する方がパフォーマンスがよい。
If TypeOf obj Is T Then
Dim casted = DirectCast(obj, T)
:
End If
↓
Dim casted = TryCast(obj, T)
If casted IsNot Nothing Then
:
End If
CType 関数
・Object 型とほかの型との変換において、Microsoft.VisualBasic.CompilerServices.Conversions クラス のようなVBランタイムヘルパーを使用する分、DirectCast よりパフォーマンスは落ちる。
・コンパイラによってインライン展開されるため、VBランタイムヘルパーが使用されない場合のパフォーマンスはよい。
・暗黙の型互換があれば変換できる。暗黙の型互換がない場合、たとえば Integer 型から DateTime 型への変換はコンパイルエラー、Object 型にボックス化された Integer 値から DateTime 型への変換は実行時に InvalidCastException となる。
・VB.NET の CType 関数と C# のキャスト演算子とでは、結果が異なる場合がある。たとえば、Single 型から Integer 型に変換する場合、CType 関数では小数点以下を銀行型丸め(近い方の整数に、半整数 .5 は偶数になるように丸める)によって取り除くが、C# では切り捨てられる。
データ型変換関数(CStr, CInt, ...)
・CInt(obj) と CType(obj , Integer) は等価(ほかの型についても同様)。
・CInt(CType)の小数→整数は銀行型丸め。
▼ヘルパークラス/メソッドによる変換
Parse, ParseExact, TryParse, TryParseExact メソッド
・文字列から変換。
・DateTime 型の場合、ParseExact で書式を明示指定して変換することができる。
・Nothing を渡すと ArgumentNullException が発生する。
ToString メソッド
・文字列化。
・Enum の ToString() はメンバ名になる。
・Decimal の ToString で末尾のゼロを取り除く(1.0 → "1")には、書式 "G29" を指定する。(.NET 1.1 以上の場合)
Convert クラス
・基本データ型を別の基本データ型に変換する。
・内部的に Parse メソッドを呼んでいる。
・小数から整数への変換は銀行型丸め。
TypeConverter クラス
・コントロールのデザインプロパティの文字列解析などに使用されている。
・TypeDescriptor.GetConverter(Type) メソッドで、実行時に型(Type オブジェクト)を指定して TypeConverter オブジェクトを取得することができる。
・文字列からの変換に ConvertFromString(String) メソッドが用意されているが、既定で用意されたコンバーターの許容範囲は狭い。桁区切りカンマ、通貨記号、全角は数値の構成要素として解釈されず、小数から整数へのキャストもできない。
Microsoft.VisualBasic.Conversion.Int メソッド
・負の値で切り捨てにならない。負でも切り捨てるには Fix 関数を使う。
Microsoft.VisualBasic.Conversion.Val メソッド
・VB6時代から想定外の挙動で何かと問題を引き起こしてきた。Val("1,234") の結果は Double の 1 である。
・手軽で許容範囲が広い反面、意図しない変換結果になることがあるので、使用には注意が必要。
▼暗黙的な変換
暗黙の数値変換(.NET 共通)
・暗黙的に変換可能な型の対応が「.NET の型変換の表 | Microsoft Docs」に拡大変換/縮小変換と分けて記載されている。
・拡大変換の多くは情報を失うことなく実行できるが、浮動小数点数値型への変換では精度が損なわれるケースもある。
・縮小変換では情報が失われる可能性がある。変換元の値が変換先の型の MaxValue と MinValue の範囲外にある場合は OverflowException が発生する。
・Integer のリテラル、定数値が変換先の型の範囲内にある場合、暗黙的に変換できる。範囲外の場合はコンパイルエラーとなる。
Option Strict Off
・Off だと意図しない型変換が暗黙的に行われ、バグの温床になる。On にすることが推奨されている。
・既定は Off なので、プロジェクトを作成したら [コンパイル オプション] 設定で On にするのを忘れないようにする。
たまに忘れて後で気づく。VB6からの移行を考慮してのことなのかもしれないが、そろそろプロジェクトの既定は On に変えてほしい。
例えば、DataTable.Rows プロパティの戻り値型 DataRowCollection は IEnumerable を実装していますので、
foreach( DataRow row in table.Rows ) {}
のように列挙させることができます。
列挙できるなら LINQ で扱いたいですね。
しかし、table.Rows. と打っても LINQ の拡張メソッドは候補表示されません。
LINQ で扱うためには、ジェネリックインターフェイスの IEnumerable<T> に変換する必要があります。
変換するには Enumerable.Cast<TResult> 拡張メソッドを使用します。
以下の3つの処理はいずれも等価です。
// メソッド構文
var rowsByMethod = table.Rows.Cast<DataRow>().Where(row => row.IsNull(0));
// クエリ構文1
var rowsByQuery1 = from row in table.Rows.Cast<DataRow>() where row.IsNull(0) select row;
// クエリ構文2(暗黙的に Cast<DataRow>() が呼ばれます)
var rowsByQuery2 = from DataRow row in table.Rows where row.IsNull(0) select row;
System.Data.DataSetExtensions を参照設定に追加すると、以下のように記述することもできます。
var rowsByTable = table.AsEnumerable().Where(row => row.IsNull(0));
非ジェネリックコレクションの ArrayList も(使われる機会はあまりなくなりましたが)、
Cast することで LINQ で扱うことができます。
ただし、キャストできない型の要素が含まれると、実行時例外の原因となりますので注意が必要です。
さて、ここで問題です。次のコードを実行するとどうなるでしょうか。
ArrayList list = new ArrayList();
list.Add(0);
list.Add("a");
IEnumerable<int> query = list.Cast<int>();
bool hasItems = query.Any();
正解は、InvalidCastException が発生する――ではありません。
hasItems が true になって正常終了します。
なぜでしょうか。
それは、Cast が遅延評価される(必要になったときに実行される)からです。
Any は要素が1つでもあれば true を返しますので、1つ目の要素しか見ません。
したがって、2つ目の要素 "a" は Cast されずに終わります。
この後、例えば query.Count() を実行すると、そこで初めて InvalidCastException が発生します。
※int 型にキャスト可能な要素のみを取得したい場合は、Cast<int>() の代わりに OfType<int>() を使用します。
DataTable 内の行を抽出するにはいくつか方法があり、それぞれに特徴を持っています。
大量のデータを対象とする場合、メモリ上に作成されるインデックスの有無でパフォーマンスにかなりの差が出ます。
DataTable
Rows.Find :
PrimaryKey で検索。PrimaryKey には自動的にインデックスが付与される。
FindBy... :
型付DataSetで主キー検索する場合。
Select :
PrimaryKey や DataView によりインデックスが作成済みであればそれを利用するので高速になる。
DataView
Sort プロパティ :
指定列にインデックスが作成される。
RowFilter プロパティ :
既にインデックスが存在すれば使用する。
Sort プロパティが未指定の場合、(列ではなくて)指定した式の処理が高速化されるよう、インデックスが作成される。
Find メソッド :
DataView 内を Sort 列の値で検索する。
FindRows メソッド :
Find メソッドの複数行版。
※DataView のインデックスは コンストラクタで構築されるほか、RowFilter/Sort プロパティ設定のたびに再構築されます。
無駄のないように、RowFilter/Sort はコンストラクタで指定するようにしましょう。
※DataView の操作によって作成されたインデックスは Dispose されるまで有効で、
同じDataTable に対する別の DataView 操作にも適用されます。
※値比較の際、大文字/小文字、全角/半角を区別するには、CaseSensitive を true にしましょう。
大量のデータを高速に投入する方法として、
SQL Server には BULK INSERT ステートメントや bcp ユーティリティが用意されていますが、
これらはテキストファイルしか読めないうえ、文字列を囲む引用符を外すには FORMATFILE の作成が必要など、
決して使い勝手がよいとは言えません。
ADO.NET の SqlBulkCopy クラス を使用すると、
DataReader や DataTable から高速にデータを投入することができます。
基本的な使い方は簡単です。
// C#
using( SqlConnection connection = new SqlConnection(connectionString) )
{
connection.Open();
using( SqlBulkCopy bulkCopy = new SqlBulkCopy(connection) )
{
bulkCopy.DestinationTableName = "FooTable";
bulkCopy.WriteToServer(source); // source: DataReader/DataTable/DataRow[]
}
}
'VB.NET
Using connection As New SqlConnection(connectionString)
connection.Open()
Using bulkCopy As New SqlBulkCopy(connection)
bulkCopy.DestinationTableName = "FooTable"
bulkCopy.WriteToServer(source) 'source: DataReader/DataTable/DataRow()
End Using
End Using
ただし、これだけだと意図した動作が実現できない場合が出てきます。
ケースに応じた対処方法と注意点を、以下に記載します。
○コピー元とコピー先で列数や列順が異なる場合は、
ColumnMappings.Add("BarColumn", "BarColumn") のように、列を対応づけておく必要があります。
(たとえば、DataTable が Expression 列を含む場合、それらを除いた列を明示的に指定します。)
○タイムアウトが発生する場合、BulkCopyTimeout プロパティに適切な値を設定します。
既定では 30 秒でタイムアウトになります。
タイムアウトを発生させたくないときは、BulkCopyTimeout プロパティに 0 を指定します。
○BatchSize プロパティで一度に処理する行数を設定し、パフォーマンスを上げることができます。
ただし、一度に処理するデータが多すぎると、BulkCopyTimeout を 0 に設定してもタイムアウトが発生することがあります。
○既定では、コピー元の Identity 列値は無視され、新たに連番が振られます。
Identity 列値を保持する場合は、SqlBulkCopy のコンストラクタで SqlBulkCopyOptions.KeepIdentity を指定します。
○処理時間が長いと、デバッグ実行で ContextSwitchDeadLock が発生することがありますが、[続行] を押すと処理が続行されます。
ContextSwitchDeadLock の発生自体を抑止するには、
[デバッグ] - [例外] から [Managed Debugging Assistants] の [ContextSwitchDeadLock] をオフにします。
データ作成/更新日時などメタデータ列の値設定は共通処理にすると便利です。
Entity Framework ではどのタイミングでどのように設定すればよいでしょうか。
(データベースのトリガーにしないこと、ローカルのシステム日付を使用することの是非についてはここでは触れません。)
方法はいくつかあります。
○データバインドコントロールのイベント
ASP.NET Web フォームの場合、以下のハンドラで
e.Values(Insert の場合)または e.NewValues(Update の場合)の各カラム要素に値を設定すると反映されます。
・GridView.RowUpdating イベント
・ListView.ItemInserting/ItemUpdating イベント
・DetailsView.ItemInserting/ItemUpdating イベント
・FormView.ItemInserting/ItemUpdating イベント
○データソースコントロールのイベント
ASP.NET Web フォームの場合、以下のハンドラでエンティティを取得してプロパティに設定することができます。
・ObjectDataSource.Inserting/Updating イベント
e.InputParameters[0] にエンティティが格納されています。
・EntityDataSource.Inserting/Updating イベント
e.Entity にエンティティが格納されています。
○SaveChanges メソッド(dynamic 方式)
個別のデータや操作に依存した値を設定するのには向きませんが、データ作成/更新日時の設定であれば、
今回ご紹介した中で最も確実で実装効率のよい方法と言えます。
ここでは DbContext(EF 4.1 ~)の SaveChanges メソッドをオーバーライドして作成日時を設定する例をご紹介します。
(DbContext の部分クラスを作成して定義します。)
public partial class SampleEntities
{
public override int SaveChanges()
{
SetCreatedDateTime();
return base.SaveChanges();
}
private void SetCreatedDateTime()
{
DateTime now = DateTime.Now;
// 追加エンティティのうち、CreatedDateTime プロパティを持つものを抽出
var entities = this.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added && e.CurrentValues.PropertyNames.Contains("CreatedDateTime"))
.Select(e => e.Entity);
foreach (dynamic entity in entities)
{
entity.CreatedDateTime = now;
}
}
}
○SaveChanges メソッド(インターフェイス方式)
エンティティの部分クラス定義(インターフェイス実装)を手動で行う必要がありますが、dynamic 方式よりきれいです。
エンティティを追加したときなど、部分クラス定義を忘れないように注意が必要です。
(対策として、アプリケーション起動時やユニットテストでリフレクションを使って実装漏れを検出することなどが考えられます。)
// エンティティのインターフェイス
public interface IEntity
{
int? CreatedUserId { get; set; }
DateTime? CreatedDateTime { get; set; }
int? UpdatedUserId { get; set; }
DateTime? UpdatedDateTime { get; set; }
}
// エンティティの部分クラス定義(例えば EntityPartials.cs など)
public partial class Foo : IEntity {}
public partial class Bar : IEntity {}
:
// DbContext の部分クラス定義
public partial class SampleEntities
{
public override int SaveChanges()
{
SetMetaFields();
return base.SaveChanges();
}
private void SetMetaFields()
{
DateTime now = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<IEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedDateTime = now;
}
if (entry.State == EntityState.Added || entry.State == EntityState.Modified)
{
entry.Entity.UpdatedDateTime = now;
}
}
}
}
Code First なら EntityBase 等の抽象クラスに定義するのがいいですね。
以下は EF Core での実装例です。
public class SampleContext : DbContext
{
:
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
SetMetaFields();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
SetMetaFields();
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SetMetaFields()
{
DateTime now = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<EntityBase>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedDateTime = now;
}
if (entry.State == EntityState.Added || entry.State == EntityState.Modified)
{
entry.Entity.UpdatedDateTime = now;
}
}
}
}
// IEntity の実装漏れを検出するユニットテスト
[TestMethod]
public void EntitiesShouldImplementIEntity()
{
var entityTypes = typeof(SampleEntities)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
.Select(p => p.PropertyType.GetGenericArguments().Single());
foreach (var type in entityTypes)
{
if (type == typeof(Baz))
{
// 除外エンティティ
continue;
}
Assert.IsTrue(typeof(IEntity).IsAssignableFrom(type), String.Format("{0} は IEntity を実装していません。", type.FullName));
}
}
Entity Framework のコンテキストにおいて、トランザクションは、既定では SaveChanges() を実行したときに暗黙的に使用されます。
要件によっては、トランザクションのスコープを明示的に制御したいケースも出てくるでしょう。
ここでは EF4.1 以降の DbContext を例に、その方法をご紹介します。
※EF6 では別の方式が推奨されるようになりました。「トランザクションのスコープ制御(EF6:Model/Database First)」「トランザクションのスコープ制御(EF6:Code First)」をご覧ください。
▼複数回の SaveChanges をまたぐトランザクション
// コンテキスト
using (var context = new NorthwindEntities())
{
// SaveChanges() を実行するたびに接続が開閉され、分散トランザクションになるのを防ぐため、あらかじめ開いておく。
((IObjectContextAdapter)context).ObjectContext.Connection.Open();
// TransactionScope で囲む。
var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
// 1つめの SaveChanges()
var product = context.Products.Single(p => p.ProductID == 1);
product.ProductName = "New Product Name";
context.SaveChanges();
// 2つめの SaveChanges()
var employee = context.Employees.Single(e => e.EmployeeID == 1);
employee.Title = "New Title";
context.SaveChanges();
// まとめてコミット
scope.Complete();
}
}
TransactionScope 内で例外が発生した場合は、トランザクションがロールバックされます。
※TransactionScope を使用するには、System.Transactions.dll を参照設定する必要があります。
▼複数のコンテキストをまたぐトランザクション
既存の接続をコンストラクタに指定してコンテキストを生成することによって、複数のコンテキスト間で接続やトランザクションを共用することができます。
// 共用する接続の作成
using (var connection = new EntityConnection("name=NorthwindEntities"))
{
// SaveChanges() を実行するたびに接続が開閉され、分散トランザクションになるのを防ぐため、あらかじめ開いておく。
connection.Open();
// TransactionScope で囲む。
var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
// 1つめのコンテキスト(接続を指定して生成)
using (var context = new NorthwindEntities(connection, false))
{
var product = context.Products.Single(p => p.ProductID == 1);
product.ProductName = "New Product Name";
context.SaveChanges();
}
// 2つめのコンテキスト(接続を指定して生成)
using (var context = new NorthwindEntities(connection, false))
{
var employee = context.Employees.Single(e => e.EmployeeID == 1);
employee.Title = "New Title";
context.SaveChanges();
}
// まとめてコミット
scope.Complete();
}
}
// コンテキストの部分クラス
public partial class NorthwindEntities : DbContext
{
/// <summary>
/// コンストラクタ。
/// </summary>
/// <param name="existingConnection">コンテキストで使用する接続。</param>
/// <param name="contextOwnsConnection">false を指定すると、コンテキストが Dispose されたときに接続を Dispose しない。</param>
public NorthwindEntities(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
}
}
▼トランザクション直接操作
TransactionScope を使用せずに、DbTransaction オブジェクトを直接操作することも可能です。
using( var context = new NorthwindEntities() )
{
var connection = ((IObjectContextAdapter)context).ObjectContext.Connection;
connection.Open();
// トランザクション開始
using( var transaction = connection.BeginTransaction() )
{
try
{
var product = context.Products.Single(p => p.ProductID == 1);
product.ProductName = "New Product Name";
context.SaveChanges();
var employee = context.Employees.Single(e => e.EmployeeID == 1);
employee.Title = "New Title";
context.SaveChanges();
// コミット
transaction.Commit();
}
catch
{
// ロールバック
transaction.Rollback();
throw;
}
}
}
Entity Framework のコンテキストにおいて、トランザクションは、既定では SaveChanges() を実行したときに暗黙的に使用されます。
要件によっては、トランザクションのスコープを明示的に制御したいケースも出てくるでしょう。
EF6 ではトランザクション操作のために DbContext.Database.BeginTransaction/UseTransaction メソッドが導入され、TransactionScope よりも推奨されるようになりました。
ここでは、トランザクションの明示的なスコープ制御を EF6 の Model/Database First で行う例を示します。
▼複数回の SaveChanges をまたぐトランザクション
Database.BeginTransaction でトランザクションを開始し、その中で SaveChanges した変更をまとめて Commit します。
// コンテキスト
using (var context = new NorthwindEntities())
{
// トランザクション開始
using (var transaction = context.Database.BeginTransaction())
{
// 1つめの SaveChanges()
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
// 2つめの SaveChanges()
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
// まとめてコミット
transaction.Commit();
}
}
▼複数のコンテキストをまたぐトランザクション
あらかじめ接続を開いておいて BeginTransaction メソッドでトランザクションを開始し、その中で複数のコンテキストを操作、SaveChanges した後にまとめて Commit します。
Database/Model First では、コンテキストのコンストラクタには EntityConnection を渡す必要があります。
コンテキストを Dispose しても接続が破棄されないよう、contextOwnsConnection 引数には false を指定します。
トランザクションは Database.UseTransaction メソッドでコンテキストに渡して共用します。
// 接続準備
var workspace = NorthwindEntities.GetMetadataWorkspace();
using (var entityConnection1 = new EntityConnection("name=NorthwindEntities"))
using (var sqlConnection = entityConnection1.StoreConnection)
using (var entityConnection2 = new EntityConnection(workspace, sqlConnection, false))
{
// あらかじめ接続を開いておく。
sqlConnection.Open();
// トランザクション開始
using (var transaction = sqlConnection.BeginTransaction())
{
// 1つ目のコンテキストを操作する。
using (var context = new NorthwindEntities(entityConnection1, false))
{
context.Database.UseTransaction(transaction);
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// 別の EntityConnection を使って2つ目のコンテキストを操作する。
// ※同じ EntityConnection を使用すると InvalidOperationException が発生する。
using (var context = new NorthwindEntities(entityConnection2, false))
{
context.Database.UseTransaction(transaction);
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// まとめてコミット
transaction.Commit();
}
}
// コンテキストの部分クラス
public partial class NorthwindEntities : DbContext
{
/// <summary>
/// コンストラクタ。
/// </summary>
/// <param name="existingConnection">コンテキストで使用する接続。</param>
/// <param name="contextOwnsConnection">false を指定すると、コンテキストが Dispose されたときに接続を Dispose しない。</param>
public NorthwindEntities(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
}
/// <summary>
/// メタデータワークスペースを取得する。
/// </summary>
/// <returns></returns>
public static MetadataWorkspace GetMetadataWorkspace()
{
using (var context = new NorthwindEntities())
{
var objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.MetadataWorkspace;
}
}
}
それぞれのコンテキストには別々の EntityConnection を渡す必要があります。
2つ目以降の EntityConnection を作成するときに必要となる MetadataWorkspace オブジェクトは、コンテキストが実装する IObjectContextAdapter インターフェイスの ObjectContext プロパティから取得できます。
トランザクションのスコープの明示的な制御をEF6の Code First で行う例を以下に示します。
※Entity Framework Core でのトランザクション制御については Microsoft Docs で詳しく解説されています。
▼複数回の SaveChanges() をまたぐトランザクション
Database.BeginTransaction でトランザクションを開始し、その中で SaveChanges した変更をまとめて Commit します。
// コンテキスト
using (var context = new NorthwindContext())
{
// トランザクション開始
using (var transaction = context.Database.BeginTransaction())
{
// 1つめの SaveChanges()
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
// 2つめの SaveChanges()
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
// まとめてコミット
transaction.Commit();
}
}
▼複数のコンテキストをまたぐトランザクション
あらかじめ接続を開いておいて BeginTransaction メソッドでトランザクションを開始し、その中で複数のコンテキストを操作、SaveChanges した後にまとめて Commit します。
Model/Database First と異なり、コンテキストのコンストラクタに渡す接続は EntityConnection を介しません。
コンテキストを Dispose しても接続が破棄されないよう、contextOwnsConnection 引数には false を指定します。
トランザクションは Database.UseTransaction メソッドでコンテキストに渡して共用します。
// 接続準備
using (var sqlConnection = new SqlConnection(NorthwindContext.GetConnectionString()))
{
// あらかじめ接続を開いておく。
sqlConnection.Open();
// トランザクション開始
using (var transaction = sqlConnection.BeginTransaction())
{
// 1つ目のコンテキストで保存
using (var context = new NorthwindContext(sqlConnection, false))
{
context.Database.UseTransaction(transaction);
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// 2つ目のコンテキストで保存
using (var context = new NorthwindContext(sqlConnection, false))
{
context.Database.UseTransaction(transaction);
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// まとめてコミット
transaction.Commit();
}
}
// コンテキストクラス
public class NorthwindContext : DbContext
{
:
/// <summary>
/// コンストラクタ。
/// </summary>
/// <param name="existingConnection">コンテキストで使用する接続。</param>
/// <param name="contextOwnsConnection">false を指定すると、コンテキストが Dispose されたときに接続を Dispose しない。</param>
public NorthwindContext(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
}
/// <summary>
/// 接続文字列を取得する。
/// </summary>
/// <returns></returns>
public static string GetConnectionString()
{
using (var context = new NorthwindContext())
{
return context.Database.Connection.ConnectionString;
}
}
:
}
Entity Framework では LINQ to Entities でデータ抽出ができて便利ですが、データベース関数を使いたいケースもあります。
どのような方法が用意されているか、EFバージョンごとに見ていきましょう。
▼Entity Framework 4/5
EF5 までの Entity Framework は .NET Framework の一部として提供され、NuGet パッケージで拡張されていました。
その中にある System.Data.Objects.EntityFunctions クラス のメソッドを使用することで、標準的なSQL関数を呼び出すことができます。
たとえば、開始日から終了日までの日数を取得したいとき、
EntityFunctions.DiffDays(e.StartDate, e.EndDate)
のように記述すると、SQL Server であれば、
DATEDIFF (day, [Extent1].[StartDate], [Extent1].[EndDate])
のようなSQLに変換されます。
SQL Server 固有の組み込み関数を呼び出すには、System.Data.Objects.SqlClient.SqlFunctions クラス のメソッドを使用します。
上記の日数取得は、SQL Server のみをターゲットに SqlFunctions で記述する場合、
SqlFunctions.DateDiff("day", e.StartDate, e.EndDate)
となります。
▼Entity Framework 6
EF6 は EntityFramework.dll を中心としたオープンソースのライブラリとして .NET Framework から独立して提供されるようになりました。
EntityFunctions は System.Data.Entity.Core.Objects 空間で提供されていますが、非推奨となっています。
非推奨となった EntityFunctions クラスに代わって、DbFunctions クラス が使用できます。
DbFunctions.DiffDays(e.StartDate, e.EndDate)
と書くと
DATEDIFF (day, [Extent1].[StartDate], [Extent1].[EndDate])
のように変換されます。
SQL Server 向けの SqlFunctions クラス は System.Data.Entity.SqlServer.SqlFunctions 名前空間から提供されています。
SqlQuery メソッド を使うことで、生のSQLクエリを直接指定して実行することもできます。
▼Entity Framework Core
DbFunctions クラス が使用できます。
静的クラスだった EF6 と異なり、静的プロパティ経由でインスタンスとして提供されます。
EF.Functions.DateDiffDay(e.StartDate, e.EndDate)
と書くと
DATEDIFF(DAY, [a].[StartDate], [a].[EndDate])
のように変換されます。
FromSqlRaw 拡張メソッド を使うことで、生のSQLクエリを直接指定して実行することもできます。
SQL Server の FILESTREAM はバイナリデータを管理するのに便利な仕組みですが、Entity Framework の Code First ではまだサポートされていません(2019年2月現在)。
たとえば Entity Framework Core では、次の手順で FILESTREAM データにアクセスできるようになります。
1. インスタンスレベルで FILESTREAM を有効化します。
FILESTREAM の有効化と構成 | Microsoft Docs
2. FILESTREAM を有効化したデータベースを作成します。
FILESTREAM が有効なデータベースを作成する方法 | Microsoft Docs
3. モデルクラスに byte 配列型でプロパティを定義します。
public class FooTable
{
// :
public byte[] FileDataColumn { get; set; }
// :
}
4. 初期マイグレーションを作成します。
[パッケージマネージャーコンソールの場合]
Add-Migration <migration name> -Context SampleContext
[CLI コマンドの場合]
dotnet ef migrations add <migration name> --context SampleContext
5. Migrations フォルダ内にヘルパークラスを作成します。
UpTableAfter メソッドでは、CREATE TABLE の後処理として、FILESTREAM に必要な Id 列と制約を定義し、バイナリ列を再作成します。
DownTableBefore メソッドでは、DROP TABLE の前処理として、バイナリ列と制約を削除します。
public static class FileStreamHelper
{
public static void UpTableAfter(MigrationBuilder migrationBuilder, string tableName, string fileDataColumnName)
{
migrationBuilder.Sql($"ALTER TABLE [{tableName}] ADD [FsId] uniqueidentifier rowguidcol NOT NULL");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] ADD CONSTRAINT [UQ_{tableName}_FsId] UNIQUE NONCLUSTERED ([FsId])");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] ADD CONSTRAINT [DF_{tableName}_FsId] DEFAULT (NewID()) FOR [FsId]");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] DROP COLUMN [{fileDataColumnName}]");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] ADD [{fileDataColumnName}] varbinary(max) FILESTREAM NULL");
}
public static void DownTableBefore(MigrationBuilder migrationBuilder, string tableName, string fileDataColumnName)
{
migrationBuilder.Sql($"ALTER TABLE [{tableName}] DROP COLUMN [{fileDataColumnName}]");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] DROP CONSTRAINT [DF_{tableName}_FsId]");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] DROP CONSTRAINT [UQ_{tableName}_FsId]");
migrationBuilder.Sql($"ALTER TABLE [{tableName}] DROP COLUMN [FsId]");
}
}
6. 初期マイグレーションクラスの Up メソッド末尾にヘルパーメソッド呼び出しを追記します。
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
:
migrationBuilder.CreateTable(
name: "FooTable", ...
:
FileStreamMigration.UpTableAfter(migrationBuilder, nameof(FooTable), nameof(FooTable.FileDataColumn));
}
7. 初期マイグレーションクラスの Down メソッド先頭にヘルパーメソッドを呼び出しを追記します。
protected override void Down(MigrationBuilder migrationBuilder)
{
FileStreamMigration.DownTableBefore(migrationBuilder, nameof(FooTable), nameof(FooTable.FileDataColumn));
:
migrationBuilder.DropTable(
name: "FooTable");
:
これで byte 配列を介した Transact-SQL による FILESTREAM アクセス が実現できます。
Ignore 方式(検討を推奨)
ファイルサイズに対してよりスケーラブルな 「ファイル システム ストリーミング」(Win32 API)方式 でアクセスする場合は、以下のようにします。
・DbContext クラスの OnModelCreating メソッドでファイルデータ列を Ignore する。
・FileStreamMigration.UpTableAfter メソッドから DROP COLUMN のステートメントを除去する。
・バイナリデータ列値の取得(SELECT)、保存(UPDATE)ロジックを別途実装する。
一覧で取得することがある場合は、全体でかなりのデータ量になることもありますので、この方式をお薦めします。
Hyperlink 要素を使うと NavigateUri プロパティにパスを指定することでページ遷移を実現することができますが、Button コントロールには NavigateUri プロパティがありません。
どのようにページを遷移させればよいでしょうか。
すぐに思いつくのは、ページのコードビハインドに Click イベントハンドラを実装して NavigationService.Navigate を呼び出すことです。
ただ、MVVM(Model-View-ViewModel)パターンを採用する場合、なるべくコードビハインドは汚したくありません。
ここではコードビハインドを使わずにページを遷移させる方法を3つご紹介します。
1. ビューモデルで遷移先のページインスタンスを指定する
NavigationWindow にホストされたページをコマンドバインディングで遷移させる例です。
[ビューモデル]
ビューからコマンドを受けて遷移を実行します。
Application.Current.MainWindow から NavigationWindow を取得し、Navigate メソッドの引数に Page インスタンスを渡しています。
※ICommand の実装(RelyCommand/DelegateCommand/独自 Command 実装)については説明を省略させていただきます。
public ICommand NavigateNextCommand { get; protected set; }
:
public FirstPageViewModel()
{
this.NavigateNextCommand = new RelayCommand<object>(this.NavigateNext);
}
:
protected void NavigateNext(object parameter)
{
var navigationWindow = (NavigationWindow)Application.Current.MainWindow;
navigationWindow.Navigate(new SecondPage(), parameter);
}
[ビュー]
ページのXAMLでボタンにビューモデルのコマンドをバインドします。
<Page.Resources>
<vm:FirstPageViewModel x:Key="PageViewModel" />
</Page.Resources>
<Page.DataContext>
<StaticResourceExtension ResourceKey="PageViewModel" />
</Page.DataContext>
:
<Button Content="次へ" Command="{Binding NavigateNextCommand}" CommandParameter="パラメータも渡せます" />
CommandParameter でパラメータを渡すこともできます。
渡したパラメータは、たとえば NavigationService.LoadCompleted の NavigationEventArgs から受け取ることができます。
(遷移先のビューモデルでパラメータを受け取るためには、それをサポートするMVVMフレームワークを採用するか、自前で渡す仕組みを実装する必要があります)
2. ビューモデルで遷移先のページ相対パスを指定する
ビューモデルでコマンドを受けた後、Navigate メソッドの引数にアプリケーションルートからの相対パスを渡します。
protected void NavigateNext(object parameter)
{
var navigationWindow = (NavigationWindow)Application.Current.MainWindow;
var uri = new Uri("Views/SecondPage.xaml", UriKind.Relative);
navigationWindow.Navigate(uri, parameter);
}
ビューモデルのほかの部分やXAMLは「1」と同じです。
3. ビヘイビアを使用してXAMLで遷移先を指定する
ビヘイビアを定義しておけば、ビューモデルへの記述も不要となり、ビューで指定するだけで遷移できるようになります。
[ビヘイビア]
ボタンコントロールにナビゲーション機能を提供するビヘイビアを定義します。
・遷移先ページ指定用に Uri 型の依存関係プロパティを定義し、XAMLから指定できるようにします。
・Click イベントハンドラで NavigationService を取得して Navigate メソッドを呼び出します。
※Behavior<T> クラスを使用するために Expression.Blend.Sdk を NuGet しておきます。
public class NavigateButtonBehaivior : Behavior<ButtonBase>
{
public static readonly DependencyProperty NavigatePageProperty =
DependencyProperty.Register("NavigatePage", typeof(Uri), typeof(NavigateButtonBehaivior), new UIPropertyMetadata(null));
public static readonly DependencyProperty NavigateExtraDataProperty =
DependencyProperty.Register("NavigateExtraData", typeof(object), typeof(NavigateButtonBehaivior), new UIPropertyMetadata(null));
// 遷移先のページ
public Uri NavigatePage
{
get { return (Uri)GetValue(NavigatePageProperty); }
set { SetValue(NavigatePageProperty, value); }
}
// 遷移先に渡すパラメータ
public object NavigateExtraData
{
get { return GetValue(NavigateExtraDataProperty); }
set { SetValue(NavigateExtraDataProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.Click += this.AssociatedObjectClick;
}
protected override void OnDetaching()
{
this.AssociatedObject.Click -= this.AssociatedObjectClick;
base.OnDetaching();
}
// クリックされたときの処理
private void AssociatedObjectClick(object sender, RoutedEventArgs e)
{
if (this.NavigatePage == null)
{
return;
}
var button = (ButtonBase)sender;
var navigationService = GetNavigationService(button);
if (navigationService == null)
{
return;
}
// 現ページのパッケージURLを取得して相対パスを絶対パスに変換する。
// ※new Uri(((IUriContext)navigationWindow).BaseUri, this.NavigatePage) だと
// ナビゲーションウィンドウXAMLからの相対パスになるので、サブディレクトリとの間で遷移できない。
var baseUri = BaseUriHelper.GetBaseUri(button);
var uri = new Uri(baseUri, this.NavigatePage);
// ナビゲート
navigationService.Navigate(uri, this.NavigateExtraData);
}
protected virtual NavigationService GetNavigationService(DependencyObject element)
{
var window = Window.GetWindow(element);
if (window is NavigationWindow navigationWindow)
{
// NavigationWindow の場合
return navigationWindow.NavigationService;
}
var parent = element;
while ((parent = VisualTreeHelper.GetParent(parent)) != null)
{
if (parent is Frame frame)
{
// プレーンな(非 Navigation)Window で Frame を使用している場合
return frame.NavigationService;
}
}
return null;
}
}
[ビュー]
ページのXAMLでは、Button に子要素としてナビゲーション用のビヘイビアを追加します。
<Page
:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:b="clr-namespace:WpfNavigation.Behaviors">
<Page.Resources>
<vm:FirstPageViewModel x:Key="PageViewModel" />
</Page.Resources>
<Page.DataContext>
<StaticResourceExtension ResourceKey="PageViewModel" />
</Page.DataContext>
:
<Button Content="次へ" >
<i:Interaction.Behaviors>
<b:NavigateButtonBehaivior NavigatePage="SecondPage.xaml" NavigateExtraData="パラメータも渡せます" />
</i:Interaction.Behaviors>
</Button>
Uri 型のプロパティを定義すると、ReSharper を導入した環境ではXAMLデザイナでリストからページを選択できるようになります。
設定されるパス文字列は現在のビューからの相対パスとなります。
パス文字列は既定のコンバーター UriTypeConverter によって Uri 型に変換されますが、そのまま Navigate メソッドに渡しても検索することができません。
Navigate メソッドには「パッケージの URI」が必要です。
ビヘイビアでは AssociatedObjectClick で BaseUriHelper.GetBaseUri メソッドを使ってこのページパスの変換を行っています。
Windowsフォームの Dock は非常に便利ですが、
コントロールを追加していくと重なってしまったり(同じ階層のコントロールなのに)、位置関係がうまく制御できなくなったりします。
貼り付ける順序が関係しているのかと、いったん切り取って貼り付け直したりしてもダメです。
(C# では、貼り付け直すとイベントハンドラとの関連付けが解除されてしまうので注意が必要です。)
Designer ファイルを直接編集して、this.Controls.SetChildIndex の実行順序を確定したい位置の順に変えると、希望どおりの位置関係にすることができます。
(例えば、Fill の上に Bottom が重なってしまった場合、Bottom の SetChildIndex を Fill より先に実行する。)
SetChildIndex は、[最前面へ移動][最背面へ移動]で制御できるようですが、
現在のZオーダーがどうなっているのか、デザイナ上ではわからないので、Designer ソースを直接編集した方が早いと思います。
引数を強制したいので引数なしコンストラクタは使用不可にしたいことがありますが、フォームをデザイナ表示するときに基底クラスのインスタンスが生成されますので、基底クラスに引数なしのコンストラクタがないとデザイナ表示できません。
そのような場合、デザイナ用に引数なしのコンストラクタを private で定義しましょう。
private FormBase()
{
InitializeComponent();
}
protected FormBase(int param)
: this() // 引数なしのコンストラクタを呼び出して、InitializeComponent を実行する。
{
this.param_ = param;
}
ASP.NET には、知っておくと便利な静的ユーティリティメソッド/プロパティがたくさんあります。
そのうち、比較的知られていないものをいくつかご紹介します。
○サーバー/セッション/リクエスト/レスポンス
・HttpContext.Current.Server プロパティ
・HttpContext.Current.Session プロパティ
・HttpContext.Current.Request プロパティ
・HttpContext.Current.Response プロパティ
Page.Session や Page.Request を引数やコンストラクタで受け取らなくてもアクセスできます。
○物理パス
・HostingEnvironment.MapPath メソッド
仮想パスから物理パスを取得するには Server.MapPath メソッドを使用することが多いと思いますが、
Server オブジェクトがなくても取得できます。
○絶対パス/アプリケーション相対パス
・VirtualPathUtility.ToAbsolute メソッド
・VirtualPathUtility.ToAppRelative メソッド
VirtualPathUtility.ToAbsolute は、Control オブジェクトにアクセスできないとき、
ResolveUrl メソッドに代わるものとして使用できます。
○クエリ文字列解析
・HttpUtility.ParseQueryString メソッド
クエリ文字列をキーと値の NameValueCollection に解析できます。
エンコードされたキーや値は自動でデコードされます。
解析には既定で UTF-8 が使用されます。
UTF-8 以外の、たとえば Shift_JIS でエンコードされたクエリ文字列を解析する場合は、
HttpUtility.ParseQueryString(query, Encoding.GetEncoding("Shift_JIS"))
のようにエンコーディングを明示指定します。
○JavaScript 文字列エンコード
・HttpUtility.JavaScriptStringEncode メソッド
.NET 4 から使えるようになりました。
○色
・ColorTranslator.FromHtml メソッド
・ColorTranslator.ToHtml メソッド
ColorTranslator.FromHtml("#99CCFF") や ColorTranslator.FromHtml("Blue") のように
文字列から Color 構造体を取得したり、
ColorTranslator.ToHtml(Color.Red) のように Color 構造体から HTML で使用できる文字列を取得したりできます。
Response.Redirect で、URL のスペルを間違えて 404 エラー。(インテリセンスが効きません)
ディレクトリ変更の反映もれで 404 エラー。(コンパイルエラーになりません)
たまにありますね。
無駄になる時間はわずかですが、こうしたつまらないミスに集中力を削がれるのは避けたいものです。
そこで、ページクラスの型から URL を生成してリダイレクトする共通メソッドを作ってみます。
using System;
using System.Collections.Specialized;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
// 文頭のアセンブリ名を表す正規表現
private static readonly Regex assemblyNameRegex_ = new Regex("^" + Regex.Escape(Assembly.GetExecutingAssembly().GetName().Name));
public static void ResponseRedirect<T>() where T: Page
{
ResponseRedirect<T>(null);
}
public static void ResponseRedirect<T>( NameValueCollection parameters ) where T: Page
{
ResponseRedirect(typeof(T), parameters);
}
public static void ResponseRedirect( Type pageType, NameValueCollection parameters )
{
// ページクラス型の完全修飾名から先頭のアセンブリ名部分を除き、仮想パスの形式にする。
// 《例》SampleWeb.Account.Login → ~/Account/Login.aspx
string path = String.Format("~{0}.aspx", assemblyNameRegex_.Replace(pageType.FullName, "").Replace(".", "/"));
string url = BuildUrl(path, parameters);
HttpContext.Current.Response.Redirect(url, false);
}
public static string BuildUrl( string path, NameValueCollection parameters )
{
if( parameters == null || parameters.Count == 0 ) return path;
List<string> queries = new List<string>();
foreach( string key in parameters.Keys )
{
foreach( string value in parameters.GetValues(key) )
{
queries.Add( String.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(value)) );
}
}
return String.Format("{0}?{1}", path, String.Join("&", queries.ToArray()));
}
こんな風に使います。
// リダイレクト
ResponseRedirect<Account.Login>();
// パラメータ付きでリダイレクト
NameValueCollection parameters = new NameValueCollection();
parameters.Add("param", "あいうえお");
ResponseRedirect<Account.Login>(parameters);
※この ResponseRedirect メソッドをそのまま使用するには、以下の2つの条件が前提となります。
・アセンブリ名と既定の名前空間が一致している。
・aspx のパスとコードビハインドの(名前空間を含む)クラス名の同期がとれている。
(既定でディレクトリごとに名前空間が付与されない VB.NET には向かない方法です。)
Entity Framework Core で add されたマイグレーションは以下のコマンドでデータベースに手動適用できます。
[パッケージマネージャーコンソール]
Update-Database -Context FooContext
[CLI コマンド]
dotnet ef database update --context FooContext
ASP.NET Core でアプリケーション起動時にマイグレーションを自動適用させるためには、Program.Main メソッドに次のように記述します。
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build(); // ASP.NET Core 2.1~ の場合
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<FooContext>();
// 自動マイグレーション適用
context.Database.Migrate();
// 初期データ生成
// ※Data フォルダ内にカスタムクラスを作成してデータ生成コードを記述しておく。
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "データベース初期構成中にエラーが発生しました。");
}
}
host.Run();
}
※Startup.Configure に記述すると Add-Migration で実行されてしまいます。(ASP.NET Core 2.0 で確認)
SQLServerへのデータインポートにはいくつか方法があります。
それぞれの特徴をまとめてみました。
▼SSMA(SQL Server Migration Assistant)
・速い。
・定義も移行できる。
・「for Access」ではアップサイジングウィザードのバグも修正されている。
・APIやコマンドラインツールが用意されていない。
▼BULK INSERT
・他 DBMS から直接読めない(テキストファイルを介す)。
・CSV の文字列引用符も値に含めてロードしてしまう(引用符を外すには FORMATFILE の作成が必要)。
▼BCP
(BULK INSERT とほぼ同じ)
▼ODBC 経由の INSERT INTO
・遅い(例:200万行で15分)。
・以下のような形式で Jet.OLEDB ドライバなどから使用できる。
INSERT INTO
[ODBC;DRIVER={SQL Server};SERVER=;DATABASE=;UID=;PWD=;].TableName
:
▼DTS
・速い(例:200万行で1分半)。
・テーブル定義は移行できるが、主キー、インデックスなど制約は移行できない。
・.NET から扱える API が提供されているが、パッケージ作成に列定義が必要で、かなり手間がかかる。
・実行端末に SQLServer がインストールされている必要がある。
▼SqlBulkCopy クラス(System.Data.SqlClient)
・速い(例:200万行で2分半)。
・他 DBMS のクエリ結果を、テキストファイルを介さずにインポートできる。(DataReader 使用)
・DataTable や DataRow[] の中身もインポート可能。
・Excel や CSV から OleDbConnection 経由でインポートする場合、データ型の自動判別に注意が必要。
MDB のCSVエクスポートは、TransferText より [Text;DATABASE=...] の外部参照を使用した方が断然高速です。
SELECT
[MdbQuery].*
INTO
[Text;DATABASE=C:;HDR=YES;FMT=Delimited;CharacterSet=OEM;].[Test.csv]
FROM
(SELECT * FROM MdbTable WHERE Id > 10) AS [MdbQuery]
《例》100万行のエクスポート
TransferText: 5分
TextDriver : 26秒
TextDriver を使うとテキストデータを加工して SQL で Oracle などの DBMS にインポートすることができます。
(注:高速ではありません)
INSERT INTO
[挿入先テーブル]
(
[列1],
[列2],
[列3]
)
IN
'' [ODBC;Driver=Microsoft ODBC for Oracle;SERVER=TNS名;UID=ユーザー名;PWD=パスワード]
SELECT DISTINCT
[Csv列1],
[Csv列2],
[Csv列3]
FROM
元ファイル.csv
WHERE
[Csv列1] IS NOT NULL // ← CSV(テキスト)ファイルが改行で終わっていたりする場合の対策
※接続文字列で「BufferSize=」を指定すると、一度にメモリに抱える行数を制御できます。
SQLServer からはリンクサーバー機能で 他の DBMS にアクセスできますが、
ODBCドライバを使って、異なる DBMS 間で JOIN させることもできます。
(大量のデータを扱う場合、パフォーマンスには注意が必要です。)
SELECT
MSSQL.*,
MDB.MdbColumn
FROM
(
SELECT
CategoryID,
MdbColumn
FROM
[MDBテーブル名]
) AS MDB
INNER JOIN
(
SELECT
*
FROM
[MSSQLテーブル名]
IN
'' [ODBC;DRIVER=SQL Server;SERVER=サーバー;DATABASE=データベース;UID=ユーザー名;PWD=パスワード;]
) AS MSSQL
ON
MDB.CategoryID = MSSQL.CategoryID
SQL Server 製品版では、定期的なバックアップをメンテナンスプランで構成することが多いと思います。
BACKUP DATABASE コマンドの出力先に共有フォルダを指定することも可能ではありますが、SQL Server 実行ユーザーに権限が必要ですし、ローカルにバックアップを出力したうえでリモートにもコピーして多重化したい、ということもあるでしょう。
また、データベースと同期をとるべきリソースファイル群も、同じタイミングでリモートにバックアップできれば便利です。
ここでは、ローカルに出力したデータベースバックアップとリソースファイル群を共有フォルダにコピーする方法をご紹介します。
バックアップのサイクルは日次で、データベースバックアップは一週間分の世代を保持するものとします。
リソースファイル群は差分のみ適用して同期をとります。
1. SQL Server 構成マネージャから、エージェントサービスの実行ユーザーをリモートアクセス権限のあるアカウントに設定します。
※仮想アカウント「NT SERVICE\SQLSERVERAGENT」の既定構成では共有フォルダにアクセスできません。
2. バックアップ先のリモートPCに共有フォルダを作成し、「1」のアカウントに変更権限を付与します。
3. Management Studio を起動してデータベースに接続します。
4. メンテナンスプランを作成します。
5. サブプランを追加してスケジュールを設定します。
6. [ツールボックス] から「T-SQL ステートメントの実行タスク」をドラッグして完全バックアップ文を登録します。
BACKUP DATABASE [Northwind] TO DISK = N'C:\Backup\SQLServer\Northwind.bak' WITH INIT, NAME = N'Northwind_backup'
※「データベースのバックアップ タスク」では、バックアップファイル名を同一にして上書きすることができません。
7. [SQL Server エージェント] から、バックアップファイルを曜日ごとにコピーするジョブを作成します。
種類:PowerShell
Copy-Item -Force -Path "C:\Backup\SQLServer\Northwind.bak" -Destination ("Microsoft.PowerShell.Core\FileSystem::\\remote-pc\Backup\SQLServer\Northwind_{0}.bak" -f (get-date).DayOfWeek)
※「(get-date).DayOfWeek」で「Sunday」など曜日文字列を付加しています。
※コピー先の共有フォルダパスは「Microsoft.PowerShell.Core\FileSystem::」で修飾します。
これをつけ忘れると、ジョブ履歴に成功と記録されるものの、実際にはコピーされません。
履歴の詳細を確認すると PowerShell で次のエラーが発生していることがわかります。
「PowerShell によって返されたエラー情報: 'コピー元パスとコピー先パスが同じプロバイダーを解決しませんでした。 」
8. [SQL Server エージェント] から、リソースファイル群の同期ジョブを作成します。
種類:PowerShell
robocopy "C:\Backup\Resources" "\\remote-pc\Backup\Resources" /E /DCOPY:T /MIR
※robocopy はコピーに成功したときの戻り値が 0 でないため、[種類] に「オペレーティング システム(CmdExec)」を指定すると既定で失敗扱いにされてしまいます。
9. サブプランのデザインに戻ります。
10. [ツールボックス] から「SQL Server エージェント ジョブの実行タスク」をドラッグして矢印で接続し、 [編集] から「7」のジョブを選択します。
11. [ツールボックス] から「SQL Server エージェント ジョブの実行タスク」をドラッグして矢印で接続し、 [編集] から「8」のジョブを選択します。
12. 作成したメンテナンスプランは [Integration Services] に接続してエクスポートできます。
[格納済のパッケージ]
[MSDB]
[Maintenance Plans]
13. バックアップフォルダのイメージ
Northwind.bak
Northwind_Friday.bak
Northwind_Monday.bak
Northwind_Saturday.bak
Northwind_Sunday.bak
Northwind_Thursday.bak
Northwind_Tuesday.bak
Northwind_Wednesday.bak
ADODB.Stream の WriteText メソッドを使用することにより、文字列の連結を高速に行うことができます。
(例:20文字×10,000行の連結が 40秒 → 0.4秒 と100倍高速化)
▼技術的背景
String はイミュータブル(不変)であり、生成後に値を変更できないため、文字列を連結する際は新たにインスタンスを生成することになります。
String の連結を繰り返すとメモリを消費し、処理速度が大きく低下するため、.NET では変更可能な文字列クラス StringBuilder の使用が推奨されています。
VB6 にはこれに相当するクラスが用意されていませんので、自前で作りました。
ADO(Microsoft ActiveX Data Objects)は、古くは MDAC(Microsoft Data Access Components)、Vista 以降は Windows DAC(Windows Data Access Components)の一部として、Windows OS に含まれています。
VB6 でデータベースにアクセスする際の標準的なライブラリとして広く使用されていますが、「ADO の基礎」に列挙されているように、データベース以外の「データソース」も扱うことができるように設計、実装されています。
ADODB.Stream クラスは、一般的にはファイルアクセスに使用することが多いと思いますが、メモリ内で使用することもできます。
▼使用イメージ
下記 StringBuilder クラスを使って、
sLines = sLines & "Line" & vbCrLf
に相当する処理を
sb.AppendLine("Line")
のように記述できるようになります。
変数宣言する場合
Dim sb As StringBuilder
Set sb = New StringBuilder
Call sb.Append("変数")
Call sb.AppendLine("を使います")
Debug.Print sb.ToString()
With ブロックで使用する場合
With New StringBuilder
Call .Append("With ブロック")
Call .AppendLine("を使います")
Debug.Print .ToString()
End With
▼VB6 版 StringBuilder クラスのソースコード
Option Explicit
'改行文字
Public Enum LineSeparatorsEnum
adCR = 13
adCRLF = -1
adLF = 10
End Enum
'ストリームオブジェクト
Private mStream As Object 'ADODB.Stream
'改行文字 ※デフォルトはvbCrLf
Public Property Get LineSeparator() As LineSeparatorsEnum
LineSeparator = mStream.LineSeparator
End Property
Public Property Let LineSeparator(ByVal e As LineSeparatorsEnum)
mStream.LineSeparator = e
End Property
'文字列の長さ
Public Property Get Length() As Long
If mStream.Size <= 0 Then
Length = mStream.Size
Else
Length = (mStream.Size / 2) - Len(vbNullChar) 'Len(ToString())だと遅い。
End If
End Property
'コンストラクタ
Private Sub Class_Initialize()
Set mStream = CreateObject("ADODB.Stream")
mStream.Type = 2 'adTypeText
LineSeparator = adCRLF
Call mStream.Open
End Sub
'文字列を追加する。
Public Function Append(ByVal s As String) As StringBuilder
Call mStream.WriteText(s, 0) 'adWriteChar
Set Append = Me
End Function
'改行付で文字列を追加する。
Public Function AppendLine(Optional ByVal s As String = "") As StringBuilder
Call mStream.WriteText(s, 1) 'adWriteLine
Set AppendLine = Me
End Function
'文字列化する。
Public Function ToString() As String
mStream.Position = 0
ToString = mStream.ReadText()
mStream.Position = mStream.Size
End Function
'デストラクタ
Private Sub Class_Terminate()
Call mStream.Close
End Sub
VB6 でも(簡易的・擬似的ではありますが)ユニットテストを実装することができます。
モックオブジェクトを利用することも可能です。
依存コンポーネント (Depend-On Component)として、上にコードを掲載した VB6 版 StringBuilder クラスを使用しますので、初めにそちらをご覧ください。
ほかに特別なサードパーティコンポーネントは使いません。
▼手法と技術的背景
― ユニットテスト ―
以下の手順でユニットテストを実装・実行できます。
1. テスト用の標準モジュールを作成します。
2. テスト用の Public メソッドを定義します。
3. テストメソッド内に対象となる処理を記述します。
4. Debug.Assert で処理結果を検証します。
5. イミディエイトウィンドウからテストメソッドを実行します。
テストメソッド呼び出しを集めたメソッドを作成しておくと、一括実行できます。
― Implements ステートメントと疑似的な継承 ―
VB6 には Implements という、インターフェイスの実装をするためのステートメントが用意されています。
.NET や Java の「インターフェイス」とは以下のような点で異なる、簡易的なゆるい仕組みです。
・インターフェイスとして指定されるのはただのクラスである。
・実装メソッドは {InterfaceClass}_{Method} という、イベントハンドラのような形式の名称で定義する。
・実装メソッドは既定では Private のため、実装クラス型の変数から実装メソッドに直接アクセスすることはできない。
(Public に変えたり、Public メソッドを通してアクセスすることはできる)
この Implements ステートメントのゆるい点をポジティブに活用すると、次のような方法で擬似的な継承を実現することができます。
1. インターフェイスとして利用するクラスを基底クラスと位置づけ、共通ロジックを記述する。
2. 実装クラスのメンバ変数(フィールド)で、インターフェイスとして利用するクラスのインスタンスを保持する。
3. 実装メソッドでは、必要に応じて「2」のインスタンスから本来のメソッドを呼び出しつつ、処理を上書きする。
▼テストコード
以下は、この方法で作成したモッククラスを利用したユニットテストの例です。
このページの「文字列連結の超高速化」でご紹介した StringBuilder クラスを使用します。
― 依存コンポーネント (Depend-On Component)のテスト ―
まず StringBuilderTest という標準モジュールを作成して StringBuilder のテストを記述します。
'StringBuilderTest.bas
Option Explicit
'Length_SingleLine
Public Sub TestStringBuilder_Length_SingleLine()
With New StringBuilder
Debug.Assert .Length = 0
Call .Append("")
Debug.Assert .Length = 0
Call .Append("あい")
Debug.Assert .Length = 2
End With
End Sub
'Length_MultiLine
Public Sub TestStringBuilder_Length_MultiLine()
With New StringBuilder
.LineSeparator = adLF
Call .AppendLine("Line")
Debug.Assert .Length = 5
End With
End Sub
'ToString
Public Sub TestStringBuilder_ToString()
With New StringBuilder
.LineSeparator = adLF
Call .Append("Code")
Debug.Assert .ToString() = "Code"
Call .AppendLine("One")
Debug.Assert .ToString() = "CodeOne" & vbLf
End With
End Sub
テストメソッドはイミディエイトウィンドウから実行します。
成功すればそのままカーソルが下の行に移動します。
検証に失敗すると、失敗した位置で実行が止まります。
― 依存コンポーネント (Depend-On Component)のモッククラス ―
次に StringBuilder のモッククラス MockStringBuilder を作成し、ユニットテスト向けに処理を上書きします。
ここでは Append, AppendLine に渡された引数値を保持し、ToString() では ToStringReturn で渡された文字列を返すようにします。
LineSeparator プロパティで実装しているように、base フィールドを使用して Moq の CallBase 的に本来の処理を呼び出すことも可能です。
'MockStringBuilder.cls
Option Explicit
'インターフェイス
' StringBuilder 型で扱えるよう、同じインターフェイスを StringBuilder_{Method} 形式で実装する。
Implements StringBuilder
'委譲インスタンス
' 擬似的な継承を実現するため、インスタンスを保持する。
Private base As New StringBuilder
'モック用のフィールド
Private mAppendCalledArgs As New Collection
Private mAppendLineCalledArgs As New Collection
Private mToStringReturn As String
'Append に渡された引数配列のコレクション
Public Property Get AppendCalledArgs() As Collection
Set AppendCalledArgs = mAppendCalledArgs
End Property
'AppendLine に渡された引数配列のコレクション
Public Property Get AppendLineCalledArgs() As Collection
Set AppendLineCalledArgs = mAppendLineCalledArgs
End Property
'ToString で返す値
Public Property Get ToStringReturn() As String
ToStringReturn = mToStringReturn
End Property
Public Property Let ToStringReturn(ByVal s As String)
mToStringReturn = s
End Property
'改行文字
Private Property Let StringBuilder_LineSeparator(ByVal e As LineSeparatorsEnum)
base.LineSeparator = e
End Property
Private Property Get StringBuilder_LineSeparator() As LineSeparatorsEnum
StringBuilder_LineSeparator = base.LineSeparator
End Property
'文字列の長さ
Private Property Get StringBuilder_Length() As Long
Debug.Assert False
End Property
'文字列を追加する。
Private Function StringBuilder_Append(ByVal s As String) As StringBuilder
'渡された引数を保持する。
Call mAppendCalledArgs.Add(Array(s))
End Function
'改行付で文字列を追加する。
Private Function StringBuilder_AppendLine(Optional ByVal s As String = "") As StringBuilder
'渡された引数を保持する。
Call mAppendLineCalledArgs.Add(Array(s))
End Function
'文字列化する。
Private Function StringBuilder_ToString() As String
'モック用の値を返す。
StringBuilder_ToString = mToStringReturn
End Function
― テスト対象システム(System Under Test) ―
StringBuilder を使用するクラス Sample です。
テキストを返す GetText メソッドが定義されています。
'Sample.cls
Option Explicit
Private mBuilder As StringBuilder
Public Property Get Builder() As StringBuilder
Builder = mBuilder
End Property
Public Property Set Builder(ByVal o As StringBuilder)
Set mBuilder = o
End Property
Public Sub Class_Initialize()
Set mBuilder = New StringBuilder
End Sub
Public Function GetText() As String
Call mBuilder.Append("Sample Calls StringBuilder")
GetText = mBuilder.ToString()
End Function
― テスト対象システム(System Under Test)のユニットテスト ―
Sample クラスの GetText メソッドをテストするのに MockStringBuilder を使用します。
'SampleTest.bas
Option Explicit
'GetText
Public Sub TestSample_GetText()
'モックオブジェクトに戻り値を設定
Dim oMockBuilder As New MockStringBuilder
oMockBuilder.ToStringReturn = "MockStringBuilder Returns"
With New Sample
Set .Builder = oMockBuilder
'対象メソッド実行
Dim sActualText As String
sActualText = .GetText()
'1回目の Append 呼び出しの第1引数を検証
Debug.Assert oMockBuilder.AppendCalledArgs(1)(0) = "Sample Calls StringBuilder"
'AppendLine は呼び出されていないことを検証
Debug.Assert oMockBuilder.AppendLineCalledArgs.Count = 0
'戻り値を検証
Debug.Assert sActualText = "MockStringBuilder Returns"
End With
End Sub
誰もがはまる「DLL 地獄」("DLL Hell")。
Windows XP SP1 以降の Window OS では、それを回避するために「Registration-Free COM」(Reg-Free COM)という仕組みが用意されており、COM(ActiveX) DLL/OCX を Side-by-Side 実行させることができます。
具体的には、マニフェストファイルを添えて配置することにより、レジストリ登録することなく、アプリケーションごとに異なるバージョンの依存コンポーネントを参照できます。
以下にその手順を説明します。
※DLL/OCX によっては対応できないケースがあるかもしれませんが、多くの場合はこの方法で構成できると思います。
▼例となるケース
[対象となる依存コンポーネントの識別子(任意)]
CodeOne.Windows.Controls
[配布したい依存コンポーネント]
CodeOne.Windows.Controls.Common.dll
CodeOne.Windows.Controls.TreeGrid.ocx
[依存コンポーネントのバージョン]
1.0.0.0
[組み込むアプリケーション]
SampleApplication.exe
▼依存コンポーネントの構成
1. 開発環境に依存ファイルを含めた対象コンポーネントをレジストリ登録しておく。
2. アプリケーションディレクトリの直下に [CodeOne.Windows.Controls] フォルダを作成する。
3. マニフェストファイル『CodeOne.Windows.Controls.manifest』を BOM 付 UTF-8 で作成し、[CodeOne.Windows.Controls] フォルダに配置する。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="x86"
name="CodeOne.Windows.Controls"
type="win32"
/>
<!-- ここに依存コンポーネントの file 要素(複数繰り返し)を記述 -->
</assembly>
4. Visual Studio(2005~)を [管理者として実行] し、「Windows フォーム」プロジェクトを新規作成する。
5. 対象の DLL/OCX を参照設定に追加し、[分離] プロパティを True に設定する。
6. 発行フォルダにローカルディレクトリ(publish など)を指定して [発行] する。
7. 発行フォルダに作成された exe.manifest の <file> 要素(複数繰り返し)のうち、依存コンポーネント分(プロジェクト自身の config を除く)をすべてコピーし、『CodeOne.Windows.Controls.manifest』の </assembly> の上に貼り付ける。
8. [CodeOne.Windows.Controls] フォルダに配布する DLL, OCX を配置する。
▼アプリケーションへの組み込み
1. 上で構成した依存コンポーネントをアプリケーションディレクトリにコピーする。
2. アプリケーションディレクトリに『SampleApplication.exe.manifest』を BOM 付 UTF-8 で作成し、コンポーネントの依存を定義する。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="x86"
name="CodeOne.SampleApplication"
type="win32"
/>
<description>サンプルアプリケーションです。</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="CodeOne.Windows.Controls"
version="1.0.0.0"
processorArchitecture="x86"
/>
</dependentAssembly>
</dependency>
<!-- 必要に応じて dependency を追加 -->
</assembly>
3. アプリケーションディレクトリに次の VB6 ランタイムファイルを配置する。
msvbvm60.dll
vb6jp.dll
※VB6 ランタイムは XP 以降の Windows OS に組み込まれている。
msvbvm60.dll は SySWOW64 に配置されているはずだが、たとえば VB6 sp6 で開発した場合、sp6 の msvbvm60.dll でないと日本語が文字化けする可能性があるため、念のためアプリケーションディレクトリに個別配置しておく。
※ADO 等のデータアクセスコンポーネント(旧 MDAC)は、Windows DAC として Vista 以降の Windows OS に組み込まれている。
4. ファイル構成は以下のようになる。
├ SampleApplication.exe
├ SampleApplication.exe.manifest
├ msvbvm60.dll
├ vb6jp.dll
└ [CodeOne.Windows.Controls]
├ CodeOne.Windows.Controls.manifest
├ CodeOne.Windows.Controls.Common.dll
└ CodeOne.Windows.Controls.TreeGrid.ocx
▼配置
Windows インストーラの場合
・プロジェクトファイル出力を削除して、EXE と manifest を手動で追加する。
・COM の [Register] プロパティはすべて vsifrNone に設定する。
▼動作の補足
レジストリ登録のない状態でアプリケーションを実行してマニフェストが読み込まれると、レジストリ登録された状態と同様に、ほかのディレクトリからも参照できるようになる。
レジストリ登録された後にマニフェストが読み込まれた場合、ほかのディレクトリから参照されるのはレジストリ登録された方になる。
exe.manifest がなくても依存コンポーネントのマニフェストが自動検出されることもあるようだが、検出されないコンポーネントもあるので配置した方がよい。
▼注意点/トラブルシューティング
・関連する複数の DLL/OCX の manifest を出力する際、重複する参照クラスがあってエラーになることがある(ActiveReportsViewer の arpro2 など)。
その場合は一方の DLL/OCX を別のプロジェクトに分けて出力し、マージしたうえで重複クラスをコメントアウトする。
・同一 progid, tlbid で clsid の異なる comClass 要素が複数作成された場合、そのまま実行するとエラーになるので、先頭以外(2 つ目以降の重複要素)をコメントアウトして動作を確認する。
・アプリケーション起動時にエラー「このアプリケーションのサイド バイ サイド構成が正しくないため、アプリケーションを開始できませんでした。」が発生する場合、<file> 単位でコメントアウトして起動を確認すると原因を絞り込むことができるが、一度起動に成功すると、コメントアウトを戻しても同じ場所ではエラーが発生しなくなる可能性があるため、注意が必要。
・.exe.manifest と依存コンポーネントの .manifest で version が合わないと実行時にエラー「プロシージャの呼び出し、または引数が不正です。」が発生する。
・COM Interop(COM 相互運用)DLLの場合、RegAsm またはマニフェストの DLL 埋め込みが必要となる。
・「このアプリケーションの構成が正しくないため、アプリケーションを開始できませんでした。」
↓
Windows XP で動かす場合、sp1 以上が必須となる。
・GrapeCity の表計算コンポーネント SPREAD の『SPR32X30.ocx』(3.0.0.31)で実行時に落ちてしまうことがある。
(イベントログの発生モジュールは「ntdll.dll」)
↓
3.0.0.52 にアップデートする。
Recordset をクライアントカーソルで取得して、
rs("列1").Properties("OPTIMIZE") = True
を実行するとメモリ上にインデックスが作成され、Filter 操作が驚異的に(ケースによっては数十倍)速くなります。
インデックス作成にかかる時間はわずかです。
※メモリ上のインデックスは、Sort プロパティに列名をセットすることでも作成されます。