「が」と「が」は等しいか?

間違い探しクイズのような感じですが、この二つの「が」は厳密には違います。

「が」 – U+304C

「が」 – U+304B U+3099

と、異なる文字で記述しています。見た目は(フォントなどによりますが)一緒です。話がややこしくなるので、1文字の方を「」、2文字の方をが」とします。
(U+3099 は、”Combining Katakana-Hiragana Voiced Sound Mark” という名前がついています。結合文字用の濁点です。)

なお、手元の環境 (Win7 + ATOK) で「が」を入力すると1文字の方の」でした。

ということは、String.Length をとると…

」 = 1

が」 = 2

となります。さらに、これらを入れたStringを比較すると…

Dim strOne As String = "が"  '---一文字の方です

Dim strTwo As String = "が"  '---二文字の方です

MsgBox(strOne = strTwo) '---FALSE が表示される

となります。確かにString オブジェクトの中身という観点では正しい挙動なのですが、見た目が同じなのでかなりややこしい問題の元になりそうです。

これをこうしてみます。

Thread.CurrentThread.CurrentCulture = New Globalization.CultureInfo("ja-JP")

MsgBox(String.Equals(strOne, strTwo)) '---FALSEが返却されます

MsgBox(String.Equals(strOne, strTwo, StringComparison.CurrentCulture)) '---TRUEが返却されます

後者のコードは、明示的に”ja-JP”カルチャーで比較するようにしたものです。

このあたりについては、MSDN : .NET Framework で文字列を使用するためのベスト プラクティス に詳しいのですが、やたらと話が入り組んでいてわかりにくいのが難点です。

今更な文字列長の話

相当に今更感があるのですが、文字列長の話です。
普通に考えれば、string.Lenth か、Len(String) 取ればいいんじゃね?と思うのですが、残念ながら…というお話です。

よーくMSDNを読んでみて下さい。
まずは、String.Length の解説
「プロパティ値」に「現在の文字列の文字数。」と書いてありますが、その下の「解説」の冒頭に、「Length プロパティは、このインスタンス内の Char オブジェクトの数を返します。Unicode 文字の数ではありません。」と書いてあります。

Char の数 ≠ 文字の数

ということです。これがどういうコトかは、後ほど改めて説明します。

お次は Len です。
こちらも「文字列内の文字数または変数を格納するために必要な公称バイト数を表す整数を返します。」と書かれています。この「または」以下がくせ者です-まるで法律の但し書き条項のようにくせ者です。
解説欄をよく読むと、まるでVB6以前のファイルの扱い方の解説を読んでいるような気分になります-FilePutなんて何時の話だと…。

どちらも何故こんな回りくどい言い回しをしているのか?何故素直に「文字列長」と言えないのか。
そもそも、VB.NETに移行することによって、 Len と LenB の混乱から逃れたのではなかったのか。

残念ながら、そうではないのです。
理由は大きく分けて二つあります。
一つは「サロゲートペア(代用対)」問題。Unicodeの16bitから溢れて、32bit(=Char 二つ分)で一文字を表す文字が存在することです。更に悪いことに、日本語にはそれにが居るということです。
もう一つは、他の言語で使われる「合成文字」です。こちらもコードとしては2byte以上(合成するだけ)必要ですが、表現する文字としては「1文字」です。濁点・半濁点を考えてもらえれば良いでしょう(実際、一部の体系ではそうですし)。

これらの問題を解決した上で「文字列」の長さをきちんと数えるには、System.Globalization.StringInfo を使用しなければいけないのです。

論より証拠。実際にやってみましょう。


こんなモノを用意してみます。一番上のテキストボックスに入れたTextの文字数を表示するというモノです。

コードはこんな感じです。ここでは実験が目的なので、何も考えずにコードビハインドに書きます。

    Private Sub cmdExec_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles cmdExec.Click
        Dim strTarget = Me.txtTest.Text
        '一つめのテキストボックスには、単純にString.Length
        Me.txtLength.Text = strTarget.Length.ToString()
        '二つ目はLEN
        Me.txtLength2.Text = Len(strTarget).ToString()
        '三つめは国際化対応
        Dim g As New System.Globalization.StringInfo(strTarget)
        Me.txtLength3.Text = g.LengthInTextElements.ToString
    End Sub

では、実行結果。

まずはとりあえず「あ」でテストしてみます。

どれも一文字と表示しています。

次はサロゲートペア(代用対)の問題です。
よく知られているのは「𠮟る」ですが、その辺りの一覧はコチラにあります。

見事に割れました。見ての通り、「文字数」としての正解は「1文字」なので、正しく値を返しているのは、System.Globalization.StringInfo のみということになります。

このサロゲートペア(代用対)は、確かに高頻度に現れる問題ではないとはいえ、ハマるとデカいという落とし穴になっています。コードを書く側もテストする側も、「お約束のヒトネタ」として押さえておくと良いでしょう。

#おまけ:[アクセサリ]→[システムツール]→[文字コード表]でも、Unicodeの直接入力が4文字まで(!)です。𠮟(U+20B9F)はどうするんだと…
#もう一つ。wordpress を使用しているのですが、素のママで𠮟(U+20B9F)を投入したら、そこで投稿が打ち切られました(内部でなんか落ちたのかなと…)。これで小一時間悩みました…。こうなりますと言うことで。

最後に、外国語の合成文字の問題です。
日本語の濁点・半濁点付きの文字も「一文字」として割り振られています。例えば、「が」は U+304C です。が、そうではない言語もあります。ここではベトナム語を取り上げます。(現代の)ベトナム語は、アルファベットと声調記号の合成で表現されます。例えば「ベトナム」のことは”Việt Nam”と書きますが、これの”ệ”は、実は合成文字で二文字です。が、実際上は1文字として認識されます。(「が」を二文字と数える日本人がどれだけ居ますか?というのと同じです。)

こちらもズレました
バイト数で切ると、文や語の途中で打ち切られる可能性があります。

と言う訳で、文字列長とバイト長は使い分けましょうというお話でした。

VB.NET で Razor記法を試す

ASP.NET MVC3 といえば Razor が重要なポイントですが、それをVB.NETでやろうとするとどうなるのか。

基本的には@の使い方なので、C#とさしたる違いはなさそうに見えます。が、そうも行かない微妙な差異があります。
C# だと .cshtml だったのが .vbhtml になるというファイルの拡張子の話もありますが、まぁそれは置いておきます。

まず、いきなり一行目から違います。

@ModelType ...[モデル名]

同じコードをC#で書くと…

@model ...[モデル名]

となります。Visual Studio からViewの追加を行った場合には自動的に追加してくれるのですが、サンプルをコピって…という場合には一瞬困ります。

次はこちらです。

@Code
    ViewData("Title") = "ホーム ページ"
End Code

これをC#にすると

@{
    ViewData["Title"] = "ホーム ページ"
}

となります。この”Code” “End Code” がなんともかんとも…という感じです。

この冒頭部を乗り越えてしまえば、.vbhtml内はC#とほとんど…と言いたいところですがそうは行きません。
たとえば、ループ処理内で詳細ページへのリンクと国名・国コードを出すようなコードはこんな感じになります。
(AdventureWorks の CountryRegion, CountryRegionCurrency を使って書いた例です。)

    @For Each country In Model
        @<tr>
            <td>@Html.ActionLink("詳細", "Detail",New With{.CountryRegionCode = country.CountryRegionCode}) </td>
            <td>@country.Name [@country.CountryRegionCurrencies.Count]
            <td>@country.CountryRegionCode</td>
        @</tr>
    Next   

Forループの書式もですが、匿名オブジェクトの作成方法と初期化方法にも注意が必要です。
匿名オブジェクトはMVC3では頻出です。New と With に気をつけてサンプルから持ってきても”.”を忘れがちです。

さらに、詳細ページでは…

        <td>@Html.DisplayFor(Function(x) x.CountryRegionCode)</td>
        <td>@Html.DisplayFor(Function(x) x.CurrencyCode)</td>

Html.DisplayFor など、引数にLinqの式をとる構文の場合、このような冗長な感じになってしまいます。
この例では一行で済んでいますが、複数行で”End Function”まで書く羽目になった日にはかなり涙目です。

やはり、VB.NETでのラムダ式は、もう少し簡素な方式を望みたいところであります。

VBで書くラムダ式

LINQはうまく使えば非常に便利なのですが、残念ながらVBで書くと非常にゴチャゴチャします。C# なら => で済む所が、Function やら Sub やら長ったらしく、しかも使い分ける必要があるのが悩みどころです。どういう感じになるか、ひとつやってみます。

お題:小田急線の列車種別・線別の停車駅をクエリして、コンソールに出力します。「正解」はコチラです。

Step1: 駅名を保持するクラスを用意します。列車種別と線名にFlags属性が付いているところに注意してください。

    Private Class Station

        <Flags()>
        Friend Enum TrainKind
            各停 = 1
            区間準急 = 2
            準急 = 4
            多摩急行 = 8
            急行 = 16
            快速急行 = 32
            特急 = 64
        End Enum

        <Flags()>
        Friend Enum Line
            小田原線 = 1
            多摩線 = 2
            江ノ島線 = 4
        End Enum

        Property 駅名 As String
        Property 線 As Line
        Property 停車 As TrainKind

    End Class

Step2:上で作ったクラスに初期値を入れておきます。 Sub main の冒頭でやっておけば十分です。(全駅を書くと大変なので、一部だけにします。)

        Dim colStations As New List(Of Station)
        With colStations
            .Add(New Station With {.駅名 = "代々木上原", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.多摩急行 Or Station.TrainKind.快速急行 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "豪徳寺", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "登戸", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.多摩急行 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "向ヶ丘遊園", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.特急 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "栗平", .線 = Station.Line.多摩線, .停車 = Station.TrainKind.急行 Or Station.TrainKind.多摩急行 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "南林間", .線 = Station.Line.江ノ島線, .停車 = Station.TrainKind.急行 Or Station.TrainKind.各停})
        End With

Step3:早速クエリを作ってみます。条件は「小田原線内の多摩急行停車駅」です。まずは、一行で条件を書いてみます。

        Dim queryInLine = colStations.Where(Function(x) (x.停車 And Station.TrainKind.多摩急行) AndAlso (x.線 And Station.Line.小田原線)) _
                            .Select(Function(y) New With {.result = y.駅名 & "[" & y.線.ToString() & "]"})

        For Each stationName In queryInLine
            Console.WriteLine(stationName.result)
        Next

一行で書こうとするとやはりゴチャゴチャします。
このうちwhereの部分だけををC# で書くとすると、

var query = colStations.Where(x => (x.停車.HasFlag(Station.TrainKind.多摩急行)) && (x.線.HasFlag(Station.Line.小田原線)));

と、かなりすっきりするのですが、ないものねだりをしても仕方がありません。

また、Selectで匿名クラスを作っています。匿名クラスのメンバーであろうと、インテリセンスは補完してくれます。

Step4:同じ条件で、複数行で書いてみます。VS2010からは「暗黙の行継続文字」という機能がついたので、狂ったように”_”を付けなくていいところが救いです。「暗黙の行継続文字」が適用される条件については、コチラをご覧下さい。

        Dim queryWithLines = colStations.Where(Function(x)
                                                   Return (x.停車 And Station.TrainKind.多摩急行) _
                                                            AndAlso _
                                                          (x.線 And Station.Line.小田原線)
                                                   End Function
                                ).ToArray()


        Array.ForEach(queryWithLines, Sub(station As Station)
                                      Console.WriteLine(String.Format("{0} [{1}]", station.駅名, station.線))
                                  End Sub)

確かに冗長ですが、見通しはかなり良くなりました。

なお、Array.ForEachの中で直接Console.WriteLineを呼んでで見ましたが、ここは”Sub”であるところに注意してください。つまり、値を返さないので、”Function”ではないのです。この使い分けが若干面倒ではあります。

Step5:最後に、デリゲートを使ってみます。

                Dim fncFilter = Function(x As Station)
                            Return (
                                (x.停車 And Station.TrainKind.多摩急行) _
                                AndAlso _
                                (x.線 And Station.Line.小田原線))
                            End Function

        Dim fncWriteLine = Sub(x As Station)
                               Console.WriteLine(String.Format("{0} [{1}]", x.駅名, x.線))
                           End Sub

        Dim queryUseDeligate = colStations.Where(Function(x) fncFilter(x))
        Array.ForEach(queryUseDeligate.ToArray(), Sub(x As Station) fncWriteLine(x))

C#の「匿名メソッド」のようなものです(コチラによると厳密には違うそうですが)。ここでは細かいことは気にせず、「こういうやり方もありますよ」ということで。
デリゲートの変数名とか書く場所に気をつけてあげれば、見通しの良いコードになりそうな気がします。

文字列の数値変換

受け取った文字列を数値にしなければいけない時、ざっと3通りの方法が用意されています。

  • Integer.Parse(String)
  • Convert.ToInt32(String)
  • CInt(String) または CType(String, typename)  VBのみ

これらがどう違うのか、実験コードを書いてみます。

Sub Main()

    Console.WriteLine("Input number...")
    Dim strOriginal As String = Console.ReadLine()

     Try
        Dim intCastResult As Integer = Integer.Parse(strOriginal)
        Console.WriteLine("int.Parse : {0}", intCastResult)
    Catch ex As Exception
        Console.WriteLine("int.Parse failed.")
    End Try

    Try
        Dim intConvert As Integer = Convert.ToInt32(strOriginal)
        Console.WriteLine("Convert.ToInt32 : {0}", intConvert)
    Catch ex As Exception
        Console.WriteLine("Convert.ToInt32 failed.")
    End Try

    Try
        Dim intCtype As Integer = CType(strOriginal, Integer)
        Console.WriteLine("Ctype : {0}", intCtype)
    Catch ex As Exception
        Console.WriteLine("Ctype failed.")
    End Try

    Console.WriteLine("Hit any key...")
    Console.ReadKey()
End Sub

これを実行してみると、以下のようになります。
“123,456” (カンマ区切りされた数値)
Input number…
123,456
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 123456
Hit any key…

“\123456″ (通貨記号をつけてみた)
Input number…
\123456
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 123456
Hit any key…

“$123456″ (ではドル記号で)
Input number…
$123456
int.Parse failed.
Convert.ToInt32 failed.
Ctype failed.
Hit any key…

“123.456,45” (海外の3桁区切り)
Input number…
123.456,45
int.Parse failed.
Convert.ToInt32 failed.
Ctype failed.
Hit any key…

“&hFE” (では16進で)
Input number…
&hFE
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 254
Hit any key…

CTypeは地域ロケールに依存しつつも、出来る限り変換しようと試みるようです。これが良い場合もあれば悪い場合もあるでしょう。

さらに、もう一歩突っ込んで、Convert.ToInt32 と int.Parse の違いを見てみましょう。
Convert の方は、IFormatProvider を引数に取ることができます。
Parse も IFormatProviderを引数に取ることができますが、NumberStyles列挙体も引数に取ることができます。例えば円記号とカンマ区切りを許容したいのであれば、

Dim intCastResult As Integer = Integer.Parse(strOriginal, Globalization.NumberStyles.AllowThousands Or Globalization.NumberStyles.AllowCurrencySymbol)

と書けることになります。
が、やはりこれも地域ロケールに依存します。
ロケールに依存させたくないというのであれば、最後はIFormatProviderということになるでしょう。

参考:
Int.Parse (MSDN)
Convert.ToInt32 (MSDN)
CType 関数 (MSDN)

VBサンプルコード まとめページ

 まぁVBのサンプルコードの類は、普通にMSDNとか、然るべき定番サイトとか行けば色々とあるのですが。
コチラはMicrosoft VBチーム謹製のサンプルコードまとめ。
リンク先が更にまとめサイトになっていたりもします。

リンク先はコチラ
http://blogs.msdn.com/b/vbteam/archive/2011/01/10/visual-basic-code-examples.aspx

ただ、カテゴリーのアイコンをクリックすると、そのアイコンのページ(つまり画像自体)が表示されるのがなんとも…

自家製ObjectをTreeView等に突っ込む時の注意点

軽めに嵌りそうになったのでメモ
MSDN-Windows Presentation Foundation データ テンプレートの概要
つまり、TreeViewとかListBoxとかが表示する時にはデフォでToStringを使うってこと。
ここで軽めに嵌ったのは、
○:Public Overrides
×:Public Overloads
×:Friend Overloads
コンパイルエラー:Friend Overrides
というポイント。むやみやたらとスコープを狭くするクセのある人は注意。

Try-Catch のネストに関するメモ

こんなネストを書くのは如何かと思いながらも、こんなコード。
[VB.NET]

    Sub Main()
        Try
            Try
                Throw New ApplicationException(“test”)
            Catch ex As Exception
                Console.WriteLine(“Caught: Level 2″)
                Console.WriteLine(ex.Message)
            End Try
            Console.WriteLine(“End of normal line of Level 1″)
        Catch ex As Exception
            Console.WriteLine(“Caught: Level 1″)
            Console.WriteLine(ex.Message)
        End Try
        Console.WriteLine(“Hit any key…”)
        Console.ReadKey()
    End Sub

で、コレの結果は

Caught: Level 2
test
End of normal line of Level 1
Hit any key…

Exceptionは握り潰されました。
じゃぁこれは?

    Sub Main()
        Try
            Try
                Throw New ApplicationException(“test”)
            Catch ex As Exception
                Console.WriteLine(“Caught: Level 2″)
                Console.WriteLine(ex.Message)
                Throw ’<-これを追加             End Try             Console.WriteLine("End of normal line of Level 1")         Catch ex As Exception             Console.WriteLine("Caught: Level 1")             Console.WriteLine(ex.Message)         End Try         Console.WriteLine("Hit any key...")         Console.ReadKey()     End Sub

結果は…

Caught: Level 2
test
Caught: Level 1
test
Hit any key…

こっちはExceptionが伝播しました。
どっちが欲しいのかは状況によって代わるので、きちんと理解しておかないと後で辛いと。

List(Of T).Remove で要素が見つからなかった時

List(Of T) でも、Dictionary(Of TKey,TValue) でも、要素を削除するのに Remove メソッドを使う訳ですが、その要素が見つからなかった場合でも、Exception は Throw されません。
Microsoft.VisualBasic.Collection.Remove や System.Collections.SortedList.Remove は Exception を投げてくれます。
その代わりに、動作結果を Boolean で返却してくれます-削除すべき要素が無かったら、”FALSE”を返してくれます。
( System.Collections.ArrayList は何も返してくれません。)
なので、List(Of T) や Dictionary(Of TKey,TValue)で「きちんと削除されたか確認したい」場合には、返値を判断することになります。
なんでこんなことを書いているのか。先日、「RemoveしているはずなのにRemoveされていない」という事象に出会ったのですが、そのコード(判りやすくするためにかなり簡略化しています)。

[VB.NET]
‘HogeCollection は List(Of Hoge)
Dim lstHoge As New HogeCollection
lstHoge.Add(New Hoge(“1″,”1″))
lstHoge.Add(New Hoge(“2″, “2”))
‘中略…
lstHoge.Remove(New Hoge(“1″,”1″))
‘↑
‘ここが問題箇所:Remove されません

この問題箇所で返値を見てみると、FALSEになっている訳です。
で、これをこんな風に直してみました。
[VB.NET]
‘HogeCollection は List(Of Hoge)
Dim lstHoge As New HogeCollection
lstHoge.Add(New Hoge(“1″,”1″))
lstHoge.Add(New Hoge(“2″, “2”))
‘中略…
lstHoge.Remove(lstHoge.Item(“1″,”1″))
‘↑
‘中から探し出してきてRemoveする。これは動く。

各メンバが全く同じオブジェクトを作って、それに「替え玉」させようったって、それはいけませんよ-という例でした。

DirectoryServices の HRESULT 一覧

DirectoryServices でドメインコントローラーとあれこれやり取りをしようとして失敗すると Exception が返ってくるわけです。
それは良いとして、ex.Message が珍妙な内容であることが多いのです。
「サーバーは利用できません。」…いや、サーバー動いてるし。
「サーバーから紹介が返ってきました。」…出会い系spamですか?
で、よく見ると、HRESULT の値が入っているわけです。
中身はCOMで動いているらしいので、伝統のHRESULTが返却されている訳ですね。
で、それぞれの値が何を意味するのか。探してみたところ、よい一覧がありました。原因と対策付です。英語です。
Computer Performance より、Logon Scripts→”Help with Logon Scripts” の 800xxxx Error Codes です。
番号順に記述されているので、非常にわかりやすいです。
但し、記述されている番号は16進表記です。したがって、Visual Studio 内や実行時に表示される10進表記を16進変換してから調べる必要があります…が、そこは、Windows付属の「電卓」が変換してくれます(16進なんか入力できないよって人は、表示(V)→関数電卓(S)をどうぞ)。
また、同様に”Help with Logon scripts” にある LDAP Properties は、あの呪文のようなLDAPクエリの文字列を解説してくれています。まさに “Hall of fame”という感じです。
ご紹介まで。