WebSurfer's Home

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

文字列連結とインターンプール

by WebSurfer 2018年2月26日 15:37

文字列を複数に分けて連結する場合、インターンプールとの関係で注意した方がよさそうと思ったことを書きます。(元の話は MSDN Forum のスレッド「ASP.NET MVC で SQL文の塊を静的クラスで宣言することについて」です)

長い文字列を一行で書くと可読性が落ちるので、複数の行に分けて書くいうことがあると思います。

例えば、以下のコードのように、SQL Server に発行するクエリを複数の文字列に分けて、改行して、+ 演算子で連結するというケースを考えてみます。

その際、以下のコードの s2, s3 のようにするのは止めた方がよさそうです。その理由を以下に書きます。

string s0 = 
    "SELECT aaa, bbb, ccc FROM Table1 WHERE ddd = @ddd";

string s1 = "SELECT aaa, bbb, ccc " +
            "FROM Table1 " +
            "WHERE ddd = @ddd";

string s2 = "SELECT aaa, bbb, ccc ";
      s2 += "FROM Table1 ";
      s2 += "WHERE eee = @eee";

// StringBuilder で連結した結果はインターンされない。
// ただし Asspnd の引数の個々の文字列はインターンされる
string s3 = new StringBuilder().
                Append("SELECT aaa, bbb, ccc ").
                Append("FROM Table1 ").
                Append("WHERE fff = @fff").
                ToString();

// t1 は "WHERE ddd = @ddd"
string t1 = new StringBuilder().
                Append("WHERE ddd "). Append("= @ddd").
                ToString();

// t2 は "WHERE eee = @eee"
string t2 = new StringBuilder().
                Append("WHERE eee ").Append("= @eee").
                ToString();

// t3 は "WHERE fff = @fff"
string t3 = new StringBuilder().
                Append("WHERE fff ").Append("= @fff").
                ToString();

Console.WriteLine("s0 と s1 は同一オブジェクトを指す: {0}", 
                (object)s1 == (object)s0);

Console.WriteLine("t1 はインターンされている: {0}", 
                (string.IsInterned(t1) == null));

Console.WriteLine("t2 はインターンされている: {0}", 
                (string.IsInterned(t2) == null));

Console.WriteLine("t3 はインターンされている: {0}", 
                (string.IsInterned(t3) == null));


// 結果は:
// s0 と s1 は同一オブジェクトを指す: True
// t1 はインターンされている: True
// t2 はインターンされている: False
// t3 はインターンされている: False

s0 のコードで文字列 "SELECT aaa, bbb, ccc FROM Table1 WHERE ddd = @ddd" はインターンプールに格納さます。

s1 のコードでは、"SELECT aaa, bbb, ccc ", "FROM Table1 ", "WHERE ddd = @ddd" という複数の文字列に分けて、それらを + 演算子で連結しています。しかしながら、個々の文字列がインターンされることはなく、コンパイラは連結後の結果だけを見て、s0 のコードで生成されインターンされた文字列の参照を s1 に代入するようです。

s0 と s1 は同じオブジェクトを指すこと(即ち、(object)s1 == (object)s0 は true になります)と、StringBuilder で生成した t1("WHERE ddd = @ddd" という文字列)がインターンされてないことがそれを裏付けていると思います。

しかし、s2, s3 の場合、t2, t3 がインターンされているという結果からみると、右辺の個々の文字列のオブジェクトも作られ、それらがインターンプールに格納されるという無駄なことが行われるようです。

ちなみに、インターンプールというのは、文字列を key に、ヒープ上の String オブジェクトのアドレスを value に持つハッシュテーブルのようなものだそうです。詳しくは MSDN ライブラリの記事「String.Intern メソッド」の記事の解説を見てください。

寿命についてもインターンプール特有のものがあるそうです。詳しくは上の記事の「パフォーマンスに関する考慮事項」のセクションを見てください。

Tags:

.NET Framework

HttpClient で WCF サービスを呼出

by WebSurfer 2018年2月24日 23:49

先の記事「HttpWebRequest で WCF サービスを呼出」では、JSON 文字列をデータとしてやり取りする WCF サービスのメソッドを HttpWebRequest / HttpWebResponse を利用して呼び出して JSON 文字列のデータを取得し、それを逆シリアル化して C# のオブジェクトに変換する方法を書きました。

HttpWebRequest / HttpWebResponse に代えて HttpClient を利用し、同じ WCF サービスのメソッドを呼び出してデータを取得するサンプルを書きます。 (呼び出し先の WCF サービスは「WCF と jQuery AJAX」を参照)

参考にしたのは Call a Web API From a .NET Client (C#) という記事ですので合わせて見ていただくと良いと思います。

まず、先の HttpWebRequest / HttpWebResponse を利用した記事と同様に、以下のクラス / データコントラクト定義を行います。

[DataContract]
public class RootObject
{
    // GetCarsByDoorsResult は WCF サービスメソッドに付与した
    // BodyStyle = WebMessageBodyStyle.WrappedRequest による
    // ラップの名前(ラップするのはセキュリティ対策)
    [DataMember]
    public List<Car> GetCarsByDoorsResult { get; set; }
}

[DataContract]
public class Car
{
    [DataMember]
    public string Make { get; set; }

    [DataMember]
    public string Model { get; set; }

    [DataMember]
    public int Year { get; set; }

    [DataMember]
    public int Doors { get; set; }

    [DataMember]
    public string Colour { get; set; }

    [DataMember]
    public float Price { get; set; }
}

参考にした Microsoft の記事ではデシリアライズに HttpContentExtensions.ReadAsAsync<T> メソッド (HttpContent) を使っていますが、今回の例のような入れ子になったオブジェクトはデシリアライズできないようです。

なので、先の記事と同様、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズすることにしました。

HttpClient.SendAsync メソッドを利用して、JSON 文字列を WCF サービスの GetCarsByDoors(int doors) メソッドに POST 送信します。以下のコードの例では 5 ドア車を要求しています。

応答の HttpContent が HttpClient.SendAsync メソッドの戻り値として返されますので、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズします。

詳しくは以下のサンプルコードとそれに付与したコメントを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading.Tasks;
using System.Net.Http;
using System.IO;

namespace ConsoleAppHttpClientWCF
{
  class Program
  {
    static void Main(string[] args)
    {
      // GetCarsAsync メソッドの引数で 5 ドアを要求
      RootObject rootObject =
          GetCarsAsync(5).GetAwaiter().GetResult();
      ShowCars(rootObject);
    }

    static async Task<RootObject> GetCarsAsync(int doors)
    {
      using (HttpClient httpClient = new HttpClient())
      {
        string url = "http://.../GetCarsByDoors";

        // POST 送信を指定
        HttpRequestMessage request =
          new HttpRequestMessage(HttpMethod.Post, url);

        // POST 送信する JSON 文字列
        string postData = 
            "{\"doors\":" + doors.ToString() + "}";

        // Content-Type: application/json; charset=utf-8 が
        // 要求ヘッダに必要。それを POST 送信する JSON 文字
        // 列と共にここで設定
        request.Content = new StringContent(postData, 
                                          Encoding.UTF8, 
                                          "application/json");

        var response = await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
        {
          // JSON シリアライザの初期化
          DataContractJsonSerializer ser =
            new DataContractJsonSerializer(typeof(RootObject));

          // 応答のコンテンツを逆シリアル化して C# の
          // オブジェクトを取得
          RootObject rootObject =
              (RootObject)ser.ReadObject(responseStream);

          return rootObject;
        }                
      }
    }

    // コンソールに取得結果を書き出すためのヘルパメソッド
    static void ShowCars(RootObject rootObject)
    {
      foreach (Car car in rootObject.GetCarsByDoorsResult)
      {
        Console.WriteLine("Make:{0}, Model:{1}, Doors:{2}",
                          car.Make, car.Model, car.Doors);
      }
    }
  }

  /*
  結果は:
  Make:Audi, Model:A4, Doors:5
  Make:Ford, Model:Focus, Doors:5
  Make:Renault, Model:Laguna, Doors:5
  Make:Toyota, Model:Previa, Doors:5
  */
}

DataContractJsonSerializer クラスを利用したシリアル化 / 逆シリアル化については、MSDN ライブラリの記事「方法 : JSON データをシリアル化および逆シリアル化する」が参考になると思います。

なお、本題とは直接関係ないことですが、You're using HttpClient wrong and it is destabilizing your software によると、HttpClient の初期化と Dispose を繰り返すようなことをすると、socket が浪費されるという問題があるそうです。これは覚えておいた方がよさそうです。

Tags: ,

.NET Framework

EditorFor での属性の付与方法

by WebSurfer 2018年2月23日 18:32

Microsoft のドキュメント What's New in ASP.NET MVC 5.1 によると、MVC 5.1 から、HTML ヘルパー EditorFor を使っても、生成される html 要素に任意の属性を付与できるようになったそうです。(実際、自分が試した限りですが、 MVC 4 では以下に述べるいずれの方法でもダメでした)

何を今頃と言われるかもしれませんが(汗)、最近そのあたりのことを調べて自分にとっていろいろ新発見があったので、備忘録としてまとめて書いておきます。

(1) TextBoxFor の場合

LabelFor, TextBoxFor などでは、第 2 引数に匿名オブジェクトを渡して html 属性を付与することができます。例えば以下のようにすると、

@Html.TextBoxFor(m => m.LastName, 
  new { @class = "coolTextBox", data_date = "12-02-2012" })

それから生成される html 要素は以下のようになります。(見やすくなるよう改行を入れてます)

<input 
  class="coolTextBox" 
  data-date="12-02-2012" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

本題とは関係ないですが、ついでに、匿名オブジェクトを書くときの注意点を上記の TestBoxFor の設定を例にして書いておきます。

匿名オブジェクトは、上記の場合 C# のコードですから、プロパティ名は C# としての識別子の条件を満たす必要があります。class は C# では予約語ですからそのまま使うことはできません。なので @ を付与して識別子名として使えるようにしています。(コンパイラには @ は無視されて class が識別子名と見なされます)

html 要素の属性としてよくある名前で、C# の識別子として使えないケースではハイフン '-' を含む属性名があると思います。例えば上記のように data-date="12-02-2012" という属性を設定したい場合です。その場合はハイフン '-' に代えてアンダースコア '_' を使います。ASP.NET によって html に変換されると、'_' は '-' に変換されます。

(2) EditorFor の場合

EditorFor では第 2 引数を上記の TextBoxFor と同様に設定しても無視されます。例えば以下のようにすると、

@Html.EditorFor(m => m.LastName, 
  new { @class = "coolTextBox", data_date = "12-02-2012" })

生成される html 要素は以下のようになります。

<input 
  class="text-box single-line" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

上記の class="text-box single-line" はデフォルトでハードコーディングされている属性だそうです。そのあたりのことは Overwriting the class on a `Html.EditorFor` とか、その記事が参照している ASP.NET MVC 2 Templates, Part 3: Default Templates に書いてありますので興味があれば見てください。

MVC 5.1 より前ではデフォルトを変更したりそれに追加することができなかったので EditorFor に代えて TextBoxFor を使うという手段を取っていたらしいです。MSDN Forum のスレッド Override CSS for textbox in MVC4 にそのような記事があります。

ところが、MVC 5.1 からは一番上に紹介した記事にあるように、EditorFor にも任意の属性が付与できるようになりました。

以下のように、匿名オブジェクトのプロパティ名を htmlAttributes とし、それに付与したい属性の匿名オブジェクトを設定するというように、入れ子で匿名オブジェクトを設定します。

@Html.EditorFor(m => m.LastName,
  new { htmlAttributes =
    new { @class = "coolTextBox", data_date = "12-02-2012" } })

その結果生成される html 要素は以下のようになります。

<input 
  class="coolTextBox text-box single-line" 
  data-date="12-02-2012" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

デフォルトでハードコーディングされている class="text-box single-line" と、上記コードで設定した class="coolTextBox" data-date="12-02-2012" がマージされているのが分かるでしょうか。

上に紹介した記事 What's New in ASP.NET MVC 5.1 によると Bootstrap をサポートするためとのことです。それでどのようにサポートできるのかは調べ切れてません。

TextBoxFor と EditorFor でなぜ違うのかは Html.EditorFor and htmlAttributes に詳しく書いてありますので、興味があれば見てください。

Tags: , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年4月  >>
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

View posts in large calendar