C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2008/06/21 03:07
C#と諸々 Nullable 型のボックス化


GetType メソッドは Object クラスで定義されている非仮想メソッドなので、GetType メソッドを値型に対して呼び出す際はボックス化が必要


と書きましたが、もうちょい詳しく書いときます。
# 今回の記事は Nullable 型に限らない話です。

[ Object クラスに定義されている非仮想メソッド ]
Object クラスには他にも非仮想メソッドとして MemberwiseClone メソッドが定義されていますが、このメソッドを呼び出す場合も同様にボックス化が発生します。
ポイントは次の 2 点です。

・ インスタンスメソッドは自分自身を取得できる
インスタンスメソッドでは、this キーワードにて自分自身を取得できます。しかしこれは、C# の話であって、IL レベルでは、this キーワードに相当する機能は用意されていません。
ではどうやって自分自身を取得するかというと、実はメソッドの 0 番目の引数として自分自身が渡されます。

・ 非仮想メソッドはオーバーライドできない
非仮想メソッドを派生クラスがオーバーライドすることはできません。つまり、GetType メソッドはオーバーライドされていないわけです。GetType メソッドは Object クラスに定義されています。

この 2 点を踏まえて考えてみてください。
GetType() メソッドの 0 番目の引数の型、つまり GetType メソッドにおける this の型は何型でしょう?
これがボックス化の理由です。
答えは Object 型ですね。


[ コンストラクタについて ]
値型は ValueType クラスを継承します。ValueType クラスはあくまでも参照型です。
値型にはコンストラクタを定義できます (C# の場合引数を持たないコンストラクタを値型に定義することはできません)。通常、コンストラクタは基底クラスのコンストラクタを呼び出さなければいけません。そしてコンストラクタは非仮想メソッド (正確には継承されないメソッド) です。ということは、コンストラクタが呼び出されるとボックス化が発生するのでしょうか?
答えは「発生しない」です。これは、値型のコンストラクタは基底クラスのコンストラクタを呼び出さないからです (これが CLR で決められているルールなのかはちょっとわからなかったです)。


[ ValueType クラスでオーバーライドされているメソッドについて ]
「プログラミング Microsoft .NET Framework 第2版」によると、ToString メソッドや Equals メソッド等の、ValueType クラスでオーバーライドされているメソッドに対してはボックス化が行われないと書いてあります。でも、ValueType クラスは参照型です。ホントにボックス化されないのでしょうか?
答えは「される」です。つまり、「プログラミング Microsoft .NET Framework 第2版」に書かれているのことは正しくないです。やはり、ValueType は参照型だからボックス化されるのです。

当然ですが、値型自身が更に ToString メソッドをオーバーライドしている場合は、ボックス化はされません。0 番目の引数の型は値型ですので。


以降、このことについて MSIL レベルで説明をします。
まず、Int32 の値に対して ToString メソッドを呼び出すコードを見てみます。

static void Main(string[] args)
{
    int a = 10;
    a.ToString();
}

このコードは次のような MSIL コードにコンパイルされます。

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] int32 a)
    L_0000: ldc.i4.s 10
    L_0002: stloc.0
    L_0003: ldloca.s a
    L_0005: call instance string [mscorlib]System.Int32::ToString()
    L_000a: pop
    L_000b: ret
}

L_0003 と L_0005 をみると、ローカル変数 a のマネージポインタをプッシュして Int32.ToString メソッドを呼び出しています。これは特に変わったところもありません。ローカル変数 a に対して ToString メソッドを呼び出すごく普通の MSIL です。(ちなみに、マネージポインタをプッシュしている理由ですが、マネージポインタではなくローカル変数 a を直接プッシュしてしまうとローカル変数 a のコピーがプッシュされてしまうためです。)


では、次のようなコードはどうでしょうか。

static void Main(string[] args)
{
    Hoge h = new Hoge();
    h.ToString();
}

struct Hoge
{
}

このコード (の Main メソッド) は次のような MSIL コードにコンパイルされます。

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] valuetype Program/Hoge h)
    L_0000: ldloca.s h
    L_0002: initobj Program/Hoge
    L_0008: ldloca.s h
    L_000a: constrained Program/Hoge
    L_0010: callvirt instance string [mscorlib]System.Object::ToString()
    L_0015: pop
    L_0016: ret
}

L_0008, L_000a, L_0010 を見てください。
L_0008 については、先ほどのコードの L_0003 と同様ローカル変数 h のマネージポインタをプッシュしています。問題は次の L_000a と L_0010 です。

まず L_0010 ですが、Hoge.ToString を call 命令で呼び出すのではなく、 Object.ToString を callvirt 命令で呼び出しています。
もし、Hoge.ToString を call 命令で呼び出していれば、CLR は最初から Hoge.ToString を使用することを知ることができます。
しかし、Object.ToString を callvirt 命令で呼び出しているということは、遅延バインディング (実行時に変数 h の型を調べて適切なメソッドを判断) するということになります。

次に (一行戻って) L_000a ですが、この constrained が鍵を握っています。これは callvirt 命令にだけ付加することができるプリフィックスです。このプリフィックスでは、一緒に型を指定します (以降、thisType と記述)。
thisType が値型の場合、次の 2 つのケースでそれぞれ異なる動作をします。

  1. tisType が callvirt 命令で指定されているメソッドを実装していない場合
  2. tisType が callvirt 命令で指定されているメソッドを実装している場合

今見ている Hoge 構造体は、ToString をオーバーライドしていませんので 1 に該当します。
この場合、ローカル変数 h はボックス化されて ToString メソッドに渡されます。まぁ、先ほども言ったように、ValueType クラスは参照型ですから当然です。そして、Object.ToString を callvirt 命令で呼び出したことになります。つまり、メソッドの遅延バインディングが発生します。


では、Hoge 構造体で ToString メソッドをオーバーライドして再コンパイルしたとします。その場合も Main メソッドの MSIL コードは変わりません。すると 2 に該当することになります。
この場合、ローカル変数 h はボックス化されずに ToString メソッドに渡されます。これも適切な動作です。また、Hoge.ToString メソッドを call 命令で呼び出した場合と同じ動作をします。つまり、メソッドの遅延バインディングが発生しません。


なぜ、Int32 値の ToString メソッドを呼び出すコードでは、constrained プリフィックスが使用されなかったのでしょうか?
恐らくそれは、バージョン問題が発生しないからです。
今回の Hoge 構造体のように、ユーザー定義の構造体のインスタンスに対して、ValueType クラスでオーバーライドされているメソッドを呼び出す場合はバージョンの問題が発生する可能性があります。
たとえば、最初は Hoge.ToString を定義せずにコンパイルしたが、後から Hoge.ToString を定義 (オーバーライド) して再コンパイルした場合、利用側のコードはコンパイル前と後で異なる動作をしなければなりません。もし constrained プリフィックスがなければ、利用側のコードも再コンパイルしないといけなくなるわけです。
というわけで、constrained プリフィックスのおかげで、再コンパイルせずに動作を切り替えられるわけです。


# すごい疲れた


[ 参考 ]
OpCodes.Constrained フィールド (System.Reflection.Emit)
スポンサーサイト



タグ: .NET C# CLR
2008/06/20 10:53
窓際プログラマーの独り言 -C#の話題を中心に:Nullable型のType より


int? a = 10;
Type type = a.GetType();
Console.WriteLine(type.Name);

を実行すると、

Int32

って表示されるんだ。



a = null;
type = a.GetType();

だと、
「オブジェクト参照がオブジェクト インスタンスに設定されていません。」
と、例外が発生する。


へー、そんな動きするんですね。
ちょっと調べてみましたところ、CLR の動作によるもののようでした。


まず、GetType メソッドは Object クラスで定義されている非仮想メソッドなので、GetType メソッドを値型に対して呼び出す際はボックス化が必要になります (Nullable 型は値型です) 。
そして、Nullable 型をボックス化する際、CLR は特別な動作をします。
Nullable 型が (論理的に) null である場合、ボックス化を行わず (正真正銘の) null を返します。
Nullable 型が (論理的に) null でない場合、Nullable 型が保持している生 (?) の値をボックス化して返します。
ということで、Nullable 型に対して GetType メソッドを呼び出しても、生の値、あるいは null に対して呼び出されてしまうというわけです。


# ちなみに、C# コンパイラが原因と予想して調べ始めたのですが、見事に外れました ^^;
タグ: .NET C# CLR
2008/06/19 00:54
データマッパー (PofEAA) がドメインオブジェクトをロードする際、リッチコンストラクタを用いることで最初から完成されたドメインオブジェクトを生成することができる。すると、ロードのためだけにセッターを提供したりする必要がないため、スマートなインターフェイスを実現できる。
しかし、循環参照を伴うドメインオブジェクトでは、リッチコンストラクタの使用に問題が生じる。
例えば、次の Hoge クラスと Fuga クラスのような場合だ。

class Hoge
{
    readonly Fuga _f;
    public Hoge(Fuga f)
    {
        if (f == null)
        {
            throw new ArgumentNullException("f");
        }
        this._f = f;
    }
}

class Fuga
{
    readonly Hoge _h;
    public Fuga(Hoge h)
    {
        if (h == null)
        {
            throw new ArgumentNullException("h");
        }
        this._h = h;
    }
}

Hoge のコンストラクタでは Fuga のインスタンスを必要とする。Fuga のコンストラクタでは Hoge のインスタンスを必要とする。これは通常、決してインスタンス化することができない。
しかし、.NET では、コンストラクタを呼び出さずにインスタンス化することができる。そして、リフレクションを使用すればコンストラクタを後から呼び出すこともできる。
つまり、次のようなコードでリッチコンストラクタの循環参照問題は克服できる。

// using System;
// using System.Runtime.Serialization;
// using System.Reflection;

Hoge h = (Hoge)FormatterServices.GetUninitializedObject(typeof(Hoge));
Fuga f = new Fuga(h);
ConstructorInfo ctor = typeof(Hoge).GetConstructor(new[] { typeof(Fuga) });
ctor.Invoke(h, new[] { f });

なお、コンストラクタを後から呼び出す代わりに、リフレクションで直接フィールドを設定するという方法も取れる。しかし、直接フィールドを設定する理由はないし、メリットもない。むしろ、循環参照の問題さえなければ普通にリッチコンストラクタでインスタンス化しているわけなのだから、リッチコンストラクタを使うべきだろう。
2008/06/12 00:03

諸君、私はテスト駆動開発が好きだ
諸君、私はテスト駆動開発が好きだ
諸君、私はテスト駆動開発が大好きだ

レッドが好きだ
グリーンが好きだ
リファクタリングが好きだ

アジャイルプロジェクトで
ペアプログラミングで

この地上に存在するありとあらゆるテスト駆動開発が大好きだ

ユニットテストがもたらす安心感が好きだ
リファクタリングを始める時など心がおどる

シンプルなコードが好きだ
リファクタリングによって洗練されていく様など胸がすくような気持ちだった

設計が常に成長していく様が好きだ
責任が適切に分担されていく様子など感動すらおぼえる

グリーンになると思い込んでいたのにレッドだった時などもうたまらない
いつでも一瞬で全てのコードがテストできるのは最高だ

気づきをもたらしてくれるレッドが好きだ
レッドを省略して実装を進めていく人はとてもとても悲しいものだ

リファクタリングの徹底が好きだ
余計な事だと馬鹿にされるのは屈辱の極みだ

諸君 私はテスト駆動開発を 楽園の守護者の様なテスト駆動開発を望んでいる
諸君 私に付き従うテスト駆動開発好きの諸君 君たちは一体何を望んでいる?
更なるテスト駆動開発を望むか 
糞の様なテスト駆動開発を望むか?
バグの手から私たちを守ってくれる女神のようなテスト駆動開発を望むか?


「テスト駆動開発!!」 「テスト駆動開発!!」 「テスト駆動開発!!」


よろしい ならばテスト駆動開発だ

だが、古い開発プロセスが染みついた場所で変化を拒む連中に耐え続けて来た我々には
ただのテスト駆動開発ではもはや足りない!!
大テスト駆動開発を!! 一心不乱の大テスト駆動開発を!!

我々はわずかに小数
ウォーターフォール派に比べれば物の数ではない
だが諸君は一騎当千の偉大な習慣を身に付けし者だと私は信じている
ならば我らは諸君と私で総兵力100万と1人の救世主の集団となる
我らを忘却の彼方へと追いやり、悪しき習慣にしがみついている奴等を叩きのめそう
髪の毛をつかんで引きずり下ろし 眼(まなこ)をあけて思い出させよう

連中に向上心を思い出させてやる
連中に勇気を思い出させてやる
テスト駆動開発には奴らの哲学では思いもよらない楽しさがある事を思い出させてやる
1000人の偉大な習慣を身に付けし者の集団で 世界を偉大な習慣で埋め尽くしてやる

目標 プログラムの楽しさを忘れてしまった者達

Be Agile 作戦 状況を開始せよ

逝くぞ 諸君



C# でも PowerShell でもなく TDD で挑戦 (?) してみました。
(一部過激な表現が含まれていますが、ネタですのでご勘弁を)

ネタ元 : 諸君、私は ECMAScript が好きだ - IT戦記
ジェネレータ : 「諸君、私はほにゃららが好きだ」ジェネレータ
2008/06/09 00:55
PowerShell のスクリプトブロックは、クロージャをサポートしていません。
しかし (毎度のことながら)、PowerShell の柔軟さを持ってすれば、クロージャを実現することだって可能です。
今回は closure という名前の関数を作りました。この関数の引数に、クロージャとして機能させたいスクリプトブロックを渡せば、スクリプトブロックをクロージャ化できます。

例えば次のような使い方ができます。 (Wikipedia に掲載されている JavaScript のクロージャサンプルを移植)

function NewCounter
{
    $i = 0;
    return closure {
        $i++;
        return $i;
    };
}

$counter = NewCounter;
&$counter; # 1
&$counter; # 2
&$counter; # 3


次のような使い方もできます。

function NewDecorator
{
    param ([string]$decoration)
    return closure {
        param ([string]$text)
        return $decoration + $text + $decoration;
    };
}

$sharpDecorator = NewDecorator '#';
&$sharpDecorator "hoge"; # #hoge#
&$sharpDecorator "fuga"; # #fuga#
&$sharpDecorator "piyo"; # #piyo#


[ コード ]
クロージャを実現する closure 関数は次のようになっています。
この関数は更に InvokeClosureScript という関数を使用します。(正確には closure 関数が生成するスクリプトブロックの内部で使用します。)

closure 関数 + InvokeClosureScript 関数
function global:closure
{
    param ([ScriptBlock]$private:script)
    trap { break; }
   
    # 引数の妥当性検証
    if ($() -eq $script) { throw '引数 script が null です。' }
   
    # 全てのクロージャを保存するクロージャストアを作成 (ハッシュテーブル)
    if ($() -eq $global:ClosureStore)
    {
        Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
    }
    # GC に回収されているクロージャ (を格納しているハッシュテーブル要素) は、クロージャストアから削除
    ($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };
   
    # 子スコープで環境 (自動変数を除く全ての変数) を取得し保存
    $autoVariableNames =
        @(
            '$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
            'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE', 'lastWord',
            'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount', 'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel',
            'null', 'OutputEncoding', 'PID', 'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException', 'ReportErrorShowSource',
            'ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference', 'WarningPreference', 'WhatIfPreference'
        );
    $private:environment = & { return Get-Variable | ? { $autoVariableNames -notcontains $_.Name }; };

    # スクリプトと環境を組み合わせてクロージャを表す。
    $private:closure =
        New-Object 'PSObject' |
            Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
            Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;

    # GUID をキー、クロージャの弱参照を値とし、クロージャストアに保存
    $private:closureId = [Guid]::NewGuid();
    $ClosureStore.Add($closureId, [WeakReference]$closure);
   
    # クロージャを実行するスクリプトを動的な文字列操作で構築 (スクリプトに GUID を埋め込むため)
    $private:invokerText =  "InvokeClosureScript `"$closureId`" `$Args;";
    # テキストからスクリプトへ変換し、更に PSObject 化する
    $private:invoker = [PSObject](Invoke-Expression "{ $invokerText }");
    # 環境をスクリプトに結びつけることでスクリプトと環境の寿命を同一化する
    Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
   
    return $invoker;
}

function global:InvokeClosureScript
{
    param ([Guid]$private:closureId, [Array]$private:Args_)

    # 指定した GUID に関連付いているクロージャをクロージャストアから取得 (クロージャは弱参照を使用して格納されている)
    $private:closure = $ClosureStore[$closureId].Target;
    # Null なら例外
    if ($() -eq $closure) { throw '指定した ID に関連付けられたクロージャは存在しません。'; }
   
    # 関数呼び出しを動的な文字列操作で構築 (param キーワードによる引数の受け取りが正常に機能するように)
    $private:invokerText = 'param ([ScriptBlock]$private:script, [Array]$private:Args_) .$script';
    for ($private:i = 0; $i -lt $Args_.Length; $i++) { $invokerText += " `$Args_[$i]"; }
    # テキストからスクリプトへ変換
    $private:invoker = Invoke-Expression "{ $invokerText }";

    # 環境のロードとクロージャの実行は変数宣言を最小限にした子スコープで
    $private:result =
        &{
            # 環境をロード
            $Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
            # クロージャを実行
            return .$Args[0] $Args[1].Script $Args[2];
        } $invoker $closure $Args_;
    return $result;
}



[ closure 関数について ]
closure 関数は、受け取ったスクリプトブロックと環境 (自動変数を除く全ての変数) の組み合わせを一つのクロージャとして、クロージャストア (グローバルなハッシュテーブル) に保存します。クロージャストアのキーには、ランダムに生成された GUID を使用します。そして、この GUID を引数として InvokeClosureScript 関数を呼び出すスクリプト (インボーカー) を生成します。closure 関数はこのインボーカーを呼び出し元へと返します。

[ InvokeClosureScript 関数について ]
InvokeClosureScript 関数は、受け取った GUID を元にクロージャストアからクロージャ (スクリプトブロックと環境) を取得します。そして、環境をロードしてスクリプトブロックを実行するのですが、スクリプトブロックは環境がロードされるスコープと同一スコープで実行されます。これにより、本来親スコープで宣言されたはずの変数でもスクリプトブロックから変更することが可能となります。例えば冒頭の一つ目のサンプルコードでは、クロージャ (となるスクリプトブロック) の親スコープで $i が宣言されていますが、クロージャ内から $i を変更 (インクリメント) しています。
(なお、環境のロードとスクリプトブロックの実行は子スコープで行われますが、これは単に、InvokeClosureScript 関数内の変数がスクリプトブロックの実行に影響を与えないようにするためです。)

[ クロージャの寿命について ]
クロージャ (スクリプトブロックと環境) をクロージャストアに保存する際には、弱い参照を保存しています。これにより、クロージャは不要になると GC の対象になることができます。クロージャが GC に回収されただけでは、まだクロージャストアにエントリが残っていますが、次に closure 関数が呼び出された際にエントリも削除されます。
またクロージャは、インボーカーのノートプロパティとしても保存されています。これにより、インボーカーがどこからか参照されている限りは、クロージャは GC の対象になりません。
ただし、一つ注意点があります。インボーカーを変数に保持する代わりに次のように関数として保持してしまうと、(PSObject ではなく生のオブジェクトが保持されるため) ノートプロパティが失われてしまうのです。

$function:func1 = closure {}; # クロージャが GC の対象になってしまう

ノートプロパティが失われてしまうということは、クロージャが GC の対象になってしまうということです。一度変数に保持してから更に関数として保持すればノートプロパティは失われませんが、これも管理が複雑になるのでお勧めしません。


[ インボーカーのノートプロパティについて ]
インボーカーのノートプロパティにクロージャが保存されている理由はクロージャの寿命の制御のためですが、これは嬉しい副作用をもたらします。次のように、インボーカーのノートプロパティを通してスクリプトブロックと環境を確認することができるのです。

$closure1 = closure {};
$closure1.Closure.Script; # スクリプトブロックを確認
$closure1.Closure.Environment; # 環境を確認




【ダウンロード】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。

YokoKen.PowerShell.Scripts
2008/06/08 02:49
スクリプトブロックのパラメータ情報を取得する関数。

[ パラメータ ]
target
対象のスクリプトブロック。

paramName
取得するパラメータの名前。
省略した場合は全てのパラメータを取得。


[ 戻り値 ]
スクリプトブロックのパラメータ情報。


[ Get-Parameter ]
function Get-Parameter
{
    param ([ScriptBlock]$target, [string]$paramName)
    trap { break; }
   
    if ($() -eq $target) { throw New-Object "ArgumentException" @("target"); }
   
    $parameterMetadataProperty = [ScriptBlock].GetProperty("ParameterMetadata", [System.Reflection.BindingFlags]"NonPublic, Instance");
    $parameterMetadata = $parameterMetadataProperty.GetValue($target, $());
    $bindableParametersProperty = $parameterMetadata.GetType().GetProperty("BindableParameters", [System.Reflection.BindingFlags]"NonPublic, Instance");
    $bindableParameters = $bindableParametersProperty.GetValue($parameterMetadata, $());
   
    $compiledCommandParameterType = [Type]::GetType("System.Management.Automation.CompiledCommandParameter");
    $typeProperty = $compiledCommandParameterType.GetProperty("Type", [System.Reflection.BindingFlags]"NonPublic, Instance");
   
    $result =
        $bindableParameters.GetEnumerator() |
            ? { (("$paramName" -eq "") -or ($_.Key -eq $paramName)); } |
            % {
                $paramInfo = New-Object "PSObject";
                Add-Member -InputObject $paramInfo -Name "Name" -Value $_.Key -MemberType "NoteProperty";
                Add-Member -InputObject $paramInfo -Name "Type" -Value $typeProperty.GetValue($_.Value, $()) -MemberType "NoteProperty";
                return $paramInfo;
            };
    return $result;
}


[ 使用例 ]
Get-Parameter ${function:Get-Parameter};
Get-Parameter ${function:Get-Parameter} "paramName";



【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。

YokoKen.PowerShell.Scripts
2008/06/06 22:42
基本的には、下記 3 点に該当する方のブログを貼らせて頂いております。
  • 当ブログに何度かコメントを投稿して頂いた
  • コメント投稿の際にご自身のブログの URL を入力して頂いた
  • ブログの更新が続いている
もしくは、下記 3 点に該当する方のブログを貼らせて頂いております。
  • 何度かコメントを投稿させて頂いた
  • 私のコメントに返答を付けて頂いた
  • (恐らく) 私の名前を覚えて頂いた

なお、大変勝手ながら、特に断りなくリンクを貼らせて頂いております。
問題等御座いましたら、大変お手数ですが、この記事へのコメントかメールフォームにてご連絡お願い致します。コメントで連絡を頂いた場合、ご希望頂ければコメントの削除も致します。
急を要する際は、下記のいずれかの手段でご連絡お願いいたします。
  • この記事へのコメントでご連絡頂く
  • 本題はメールにてご連絡頂き、その旨をこの記事へのコメントでご連絡頂く


タグ: