WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

C# と VB.NET のフィールド初期化子の違い

by WebSurfer 2016年2月6日 15:25

C# でフィールドをインスタンスメソッド等で初期化しようとすると「フィールド初期化子は、静的でないフィールド、メソッド、またはプロパティ 'xxx' を参照できません」(xxx はそのメソッド名)というコンパイルエラーになります。

ところが VB.NET ではコンパイラは問題なく通ってしまいます。実行上も、自分が調べた限りですが、期待通り動きました。(ただし、詳しく調べたわけではないので、どんなケースでも問題ないかは分かりませんが)

理由は、想像ですが、C# と VB.NET のコンパイラの違いによるものだと思われます。

(1) Why Do Initializers Run In The Opposite Order As Constructors? Part One、(2) Can VB.NET be forced to initialize instance variables BEFORE invoking the base type constructor?、(3) Field initializer differences between C# and VB.NET 等の記事を読んでみると:

  • C# の場合: まず最初に全ての initializers が derived ⇒ base の順で実行され、次に全ての constructor bodies が base ⇒ derived の順で実行される。
  • VB.NET の場合: constructor bodies が base ⇒ derived の順で実行される。その際、各 initializer は当該 constructor body が実行される直前に実行される。

という違いがあるようです。

知ってました? 実は自分はつい最近まで知らなかったです。(汗) 知っておくべきところは以上なんですが、それだけではブログの記事として面白くないので、無知な自分がハマった話を書いておきます。(笑)

その話は、以下のような VB.NET のコードを C# に書き換えるということから始まりました。

Public Class Sample
    Public Property Code As Integer()

    Public ReadOnly Property Codes As IList(Of Integer)
        Get
            Return Me._Codes.Value
        End Get
    End Property
    
    Private ReadOnly _Codes As New Lazy(Of IList(Of Integer))(
        Function()
            Return If(Me.Code, {}).ToList()
        End Function)
End Class

VB.NET をよく知らない自分は Telerik Code Converter などの変換サービスを利用しています。

自動変換されたコードをそのまま信じて使うのは問題ありと認識していますが、かなりのところまで変換してくれるので多少の手直しで済み、重宝しています。

今回の場合では、VB.NET のコードの If(Me.Code, {}) のところで {} は変換してくれなかったので、そこのみ new int[0] に直して Visual Studio のエディタに貼り付けました。(VB.NET のコードで {} は New Integer() {} と見なされるらしいです。実は、それも知らなかったのですが、さすがに C# で {} ではダメなのは気がつきましたので直しました)

そうすると、以下の画像の赤丸で示した部分でエラーが出ます。

Visual Studio に表示されたエラー

エラーメッセージは最初の方は:「ラムダ式 はデリゲート型ではないため、型 'System.Threading.LazyThreadSafetyMode' に変換できません」(コンパイルした時のエラーメッセージは「ラムダ式 はデリゲート型ではないため、型 'bool' に変換できません。」になります)

2 つ目は:「キーワード 'this' は現在のコンテキストでは使用できません」

となりました。それらのエラーメッセージから原因は分かりますでしょうか? 無知な自分は最初原因が分からず、半日ぐらいハマってしまいました。(汗)

結局原因は、Code がインスタンスプロパティなので(静的ではないので)それを使ってフィールドを初期化することはできないということだったんですが、それなら「フィールド初期化子は、静的でないフィールド、メソッド、またはプロパティ 'Code' を参照できません」というエラーメッセージを出して欲しかったです。(泣き言)

解決策は、変数 _Codes をコンストラクタで初期化するのがよさそうです。(Code プロパティに static キーワードを付与して静的プロパティにするのは止めた方がよさそうです)

Tags:

.NET Framework

全角数字を半角に変換

by WebSurfer 2016年1月15日 17:58

全角 / 半角の変換には Microsoft.VisualBasic 名前空間の Strings.StrConv メソッドがよく使われているようですが、それは使わないで、全角数字を半角に変換する方法を書きます。

基本的には Regex.Replace メソッド (String, MatchEvaluator) を使って文字列の中の全角数字の部分を半角数字に置き換えるのですが、問題は引数の MatchEvaluator に設定するカスタムメソッドをどのように作るかです。

@IT の記事「全角英数字のみを半角に変換するには?」にその例がありますが、そこでは Strings.StrConv メソッドが使われています。

しかしながら、Strings.StrConv メソッドはローケルの違いとか XP 互換モードで期待した結果にならないという問題があるそうです。

数字だけなら辞書を作るのは簡単ですので、Strings.StrConv メソッドを使う代わりに、IEnumerable.Select 拡張メソッドと辞書を使ってカスタムメソッドを実装してみました。

以下にコード例を書きます。その中の Replacer というのが MatchEvaluator に設定するカスタムメソッドです。

Replacer のコードの中で、m.Value はマッチした全角数字の文字列(String 型。IEnumerable<char> を継承している)になります。Select 拡張メソッドは辞書を使って全角数字の文字列の各文字 (Char 型)を半角に置き換え、それを IEnumerable<char> 型のオブジェクトとして返します。

半角数字に置き換えられた IEnumerable<char> 型のオブジェクトを ToArray メソッドで Char[] に変換し、String(Char[]) コンストラクタで半角数字の文字列(String 型)を生成して Replacer メソッドの戻り値として返しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace ConsoleApplication
{
  class Program
  {
    // 辞書を作る
    public static Dictionary<char, char> dictionary =
      new Dictionary<char, char>() {
        {'0','0'},{'1','1'},{'2','2'},{'3','3'},
        {'4','4'},{'5','5'},{'6','6'},{'7','7'},
        {'8','8'},{'9','9'}
      };
        
    public static string Convert(string source)
    {
      Regex regex = new Regex("[0-9]+");
      return regex.Replace(source, Replacer);
    }

    public static String Replacer(Match m)
    {
      return new String(
        m.Value.Select(n => dictionary[n]).ToArray());
    }

    static void Main(string[] args)
    {
      String source = "0123456789Ab1C02dEfあい36";
            
      Console.WriteLine(source);
      Console.WriteLine(Convert(source));

      // Regex を使わなくても以下のようにして可能
      String replaced = new String(
        source.Select(
          n => (dictionary.ContainsKey(n) ? dictionary[n] : n)
          ).ToArray()
        );
      Console.WriteLine(replaced);
            
      /* 結果は:
      0123456789Ab1C02dEfあい36
      0123456789Ab1C02dEfあい36
      0123456789Ab1C02dEfあい36
      */
    }
  }
}

Tags: ,

.NET Framework

ポストバック前後でスクロール位置維持

by WebSurfer 2016年1月11日 14:11

ASP.NET Web Forms アプリには MaintainScrollPositionOnPostBack プロパティというものがあって、ポストバック前後で上下左右のスクロール位置を維持できる仕組みが用意されています。

ただし、ASP.NET 2.0, 3.0, 3.5 でブラウザに Safari, Chrome を使用した場合は動きません。理由は、先の記事「Safari は downlevel browser?」に書きましたように、Safari, Chrome のブラウザ定義に問題があるからです。(ASP.NET 4 は問題なし)

以下に、上下左右のスクロール位置を維持する仕組み、ASP.NET 2.0, 3.0, 3.5 での Safari, Chrome のブラウザ定義は何故ダメか、その対処方法を書きます。

まずどういう仕組みでスクロール位置を維持するかですが、Page で MaintainScrollPositionOnPostback が true に設定されていると、ブラウザ定義に問題がなければ以下のようなスクリプトと隠しフィールドが html ソースに含まれてレンダリングされます。(注:初期画面では隠しフィールドの value は "0" になり、下の方のインラインスクリプトで window.onload での操作を行う 2 行は含まれません)

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
    theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}
//]]>
</script>


<script src="/WebResource.axd?d=Dgk..." type="text/javascript">
</script>


<input type="hidden" 
  name="__SCROLLPOSITIONX" 
  id="__SCROLLPOSITIONX" 
  value="50" />
<input type="hidden" 
  name="__SCROLLPOSITIONY" 
  id="__SCROLLPOSITIONY" 
  value="200" />


<script type="text/javascript">
//<![CDATA[
theForm.oldSubmit = theForm.submit;
theForm.submit = WebForm_SaveScrollPositionSubmit;

theForm.oldOnSubmit = theForm.onsubmit;
theForm.onsubmit = WebForm_SaveScrollPositionOnSubmit;

theForm.oldOnLoad = window.onload;
window.onload = WebForm_RestoreScrollPosition;
//]]>
</script>

上のコードで真中あたりにある WebResource.axd は HTTP ハンドラで、Page の埋め込みリソースとして設定されている外部スクリプトファイルを取得します。

その外部スクリプトファイルの中に、上にアップしたコードの下の方にあるインラインスクリプトで使う WebForm_SaveScrollPositionSubmit メソッド他の定義が含まれています。(興味があれば開いて中身を見てください。Visual Studio でデバッグ実行すれば開いてブレークポイントを設定したりできます)

これらのスクリプトによって、(1) ポストバックされる前の画面のスクロール位置を取得、(2) 隠しフィールドの value に画面のスクロール位置を設定してサーバーに送信、(3) サーバーから応答が戻ってきて画面が再描画されるときに隠しフィールドからポストバック前のスクロール位置を取得、(4) 画面のスクロール位置をポストバック前と同じに設定・・・という操作を行います。

しかしながら、ブラウザ定義ファイルで capability 要素 (capabilities の子要素) の supportsMaintainScrollPositionOnPostback が true に設定されてないと ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

ASP.NET 2.0, 3.0, 3.5 用のブラウザ定義ファイルは以下のフォルダにあります。

C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\Browsers

2016/1/11 時点では mozilla.browser ファイルの中に id="Safari1Plus" という browser 要素があって、Safari(かなり古いものは除く)および Chrome はその定義の適用を受けます(継承元の Default ← Mozilla ← Gecko ← Safari の定義も)。

Default のブラウザ定義で supportsMaintainScrollPositionOnPostback は false に設定されていますが、Default ← Mozilla ← Gecko ← Safari ← Safari1Plus という継承の過程でそれを true に設定するものがありません。(注:ASP.NET 4 のブラウザ定義ファイルは問題ありません)

結果、Page で MaintainScrollPositionOnPostBack プロパティを true に設定しても ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

解決策は、アプリケーションルート直下に App_Browser フォルダを追加し、さらにそのフォルダに .browser ファイル(名前は任意)を追加し、その中で refID 属性を使用して既存の id="Safari1Plus" のブラウザー定義で supportsMaintainScrollPositionOnPostback が true になるように設定してやります。

具体的には、以下のコードを新たに作った .browser ファイルに記述します。これによって、Safari, Chrome でも必要なスクリプトや隠しフィールドが生成されるようになるはずです。

<browsers>
  <browser refID="Safari1Plus">
    <capabilities>
      <capability 
        name="supportsMaintainScrollPositionOnPostback"
        value="true" />
    </capabilities>
  </browser>
</browsers>

CONFIG\Browsers フォルダの中の mozilla.browser 他の定義済みのブラウザ定義ファイルを修正するのは NG ですので注意してください。修正しても修正結果は反映されませんので。(詳しくは MSDN Blog の記事「ASP.NET の IE10 対応について」を見てください)

Tags:

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar