2015/04/29

VBのループ中のローカル変数

今更という気はしますが、VBで引っ掛かってしまったので、記録として。

VBとC#は兄弟言語ですが、C#から見ると引っ掛かりやすい点があって、ぱっと思いつく限り以下のようなものがあります。
  • Nothingの意味(必ずしもnullではない)
  • 配列のコンストラクタの要素数
  • 整数と実数の自動変換(とくに除算時)
  • オーバーロード解決の優先順
これらとは別に、あまり意識してなかった違いとして、メソッド中のローカル変数の初期値の扱いがあります。C#ではローカル変数を宣言後、値を与えないまま使おうとするとエラーとなってビルドできないのに対し、VBでは値を与えなくてもその型の既定値を使う形でビルドできます。

以下のメソッド中のローカル変数valueについて、LocalVariableCase0では初期値としてFalseを代入しているのに対し、LocalVariableCase1では初期値を代入していませんが、Boolean型の既定値がFalseなので同じ結果になります。
Private Sub LocalVariableCase0()
Dim value As Boolean = False
Console.WriteLine(value)
value = True
'False
End Sub
Private Sub LocalVariableCase1()
Dim value As Boolean
Console.WriteLine(value) 'No error in VB
value = True
'False
End Sub
view raw Program.vb hosted with ❤ by GitHub
ここまでは問題ありませんが、これをループにすると違いが出ます。

以下のLocalVariableCase2ではループが回る度にvalueはFalseに初期化されますが、LocalVariableCase3ではループしても前回のループで与えられたTrueが残ってしまいます。
Private Sub LocalVariableCase2()
For i = 0 To 2
Dim value As Boolean = False
Console.WriteLine(value)
value = True
Next
'False
'False
'False
End Sub
Private Sub LocalVariableCase3()
For i = 0 To 2
Dim value As Boolean
Console.WriteLine(value) 'No error in VB
value = True
Next
'False
'True
'True
End Sub
view raw Program.vb hosted with ❤ by GitHub
ここで試しにLocalVariableCase3のループ中の処理を別メソッドに切り出すと、valueのスコープはそのメソッド中に限定されてLocalVariableCase2と同じ結果になります。
Private Sub LocalVariableCase4A()
For i = 0 To 2
LocalVariableCase4B()
Next
'False
'False
'False
End Sub
Private Sub LocalVariableCase4B()
Dim value As Boolean
Console.WriteLine(value) 'No error in VB
value = True
End Sub
view raw Program.vb hosted with ❤ by GitHub
つまり、同一メソッド内のループ中であればループの度にローカル変数を新たに宣言したつもりでも、そうはならず前回の値が維持されます。

これは少しトリッキーというか、予想とは違っていて驚いたわけですが、そういえばVBを勉強し始めたときに変数に初期値を与えておかないと予期しない動作になって危ないと読んだような記憶がありますが、すっかり忘れてました。

というか、何でもかんでも初期値を与えるのもカーゴカルトみたいで無駄だなと思って削っていたら引っ掛かってしまったわけですが、また忘れそうなので書いておきます。

[追記] IL

これだけでは何がどうなっているか明瞭でないので、LocalVariableCase3のILをIL DASMで見ると、こうなっています。


これも今更ですが、ローカル変数はVBでのメソッド中の位置に関わらずILでは冒頭で宣言される形になっています。そこでVBでもローカル変数を冒頭で宣言するように変えたメソッドを作り、そのILを見たのが以下です。
Private Sub LocalVariableCase5()
Dim i As Integer
Dim value As Boolean
For i = 0 To 2
Console.WriteLine(value) 'No error in VB
value = True
Next
'False
'True
'True
End Sub
view raw Program.vb hosted with ❤ by GitHub


見ての通り、ILは全く同じになります。つまり、ローカル変数の宣言はその位置で変数が初めて確保されることを意味しないので、初期値にリセットするにはきちんと代入しないとダメということですね。

2015/04/15

WinRTでの設定の保存

WinRTでの設定については、どこに、どのタイミングで保存するか考える必要があるわけですが、
  1. 設定が変更される都度、保存するようにした方が確実。
  2. 永続的にするにはApplicationDataのLocalSettingsかRoamingSettingsに保存するのが便利。
  3. 設定用クラスの設定用プロパティのアクセサー内でこれらにアクセスするようにすると管理がすっきりする。
  4. そもそもLocalSettingsかRoamingSettingsを設定用プロパティのバッキングストアにしてしまえばいい。
ということになって、@tmytさんがそういう例を出されてます。
この方法をベースに自作列挙型や自作クラスも保存できるように考えて、以下のようになりました。まずは設定用の基本クラス。
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using Windows.Foundation.Collections;
using Windows.Storage;
public abstract class SettingsBase : INotifyPropertyChanged
{
protected enum ContainerType
{
Local, // Default
Roaming,
}
protected static T GetValue<T>(ContainerType container = default(ContainerType), [CallerMemberName] string propertyName = null)
{
return GetValue(default(T), container, propertyName);
}
protected static T GetValue<T>(T defaultValue, ContainerType container = default(ContainerType), [CallerMemberName] string propertyName = null)
{
try
{
var values = GetSettingsValues(container);
if (values.ContainsKey(propertyName))
{
if (typeof(T).GetTypeInfo().GetCustomAttribute(typeof(DataContractAttribute)) != null)
{
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
{
sw.Write(values[propertyName]);
sw.Flush();
ms.Seek(0, SeekOrigin.Begin);
var serializer = new DataContractJsonSerializer(typeof(T));
return (T)serializer.ReadObject(ms);
}
}
return (T)values[propertyName];
}
}
catch (Exception ex)
{
Debug.WriteLine("Failed to get property value: {0}\r\n{1}", propertyName, ex);
}
return defaultValue;
}
protected static void SetValue<T>(T propertyValue, ContainerType container = default(ContainerType), [CallerMemberName] string propertyName = null)
{
try
{
var values = GetSettingsValues(container);
if (typeof(T).GetTypeInfo().IsEnum)
{
var underlyingValue = Convert.ChangeType(propertyValue, Enum.GetUnderlyingType(typeof(T)));
if (values.ContainsKey(propertyName))
values[propertyName] = underlyingValue;
else
values.Add(propertyName, underlyingValue);
return;
}
if (typeof(T).GetTypeInfo().GetCustomAttribute(typeof(DataContractAttribute)) != null)
{
using (var ms = new MemoryStream())
using (var sr = new StreamReader(ms))
{
var serializer = new DataContractJsonSerializer(typeof(T));
serializer.WriteObject(ms, propertyValue);
ms.Seek(0, SeekOrigin.Begin);
var serializedValue = sr.ReadToEnd();
if (values.ContainsKey(propertyName))
values[propertyName] = serializedValue;
else
values.Add(propertyName, serializedValue);
}
return;
}
if (values.ContainsKey(propertyName))
values[propertyName] = propertyValue;
else
values.Add(propertyName, propertyValue);
}
catch (Exception ex)
{
Debug.WriteLine("Failed to set property value: {0}\r\n{1}", propertyName, ex);
}
}
private static IPropertySet GetSettingsValues(ContainerType container)
{
return (container == ContainerType.Local)
? ApplicationData.Current.LocalSettings.Values
: ApplicationData.Current.RoamingSettings.Values;
}
#region INotifyPropertyChanged member
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
view raw SettingsBase.cs hosted with ❤ by GitHub
自作列挙型は基になる型に変換した上でそれを保存するように、自作クラスはDataContract属性を付ける前提でJSONにシリアライズした形で保存するようにしています。

これを継承した設定用クラスの例。
using Windows.Foundation;
public class Settings : SettingsBase
{
private Settings()
{ }
public static Settings Current
{
get { return _current; }
}
private static readonly Settings _current = new Settings();
public Point StartLocation
{
get { return GetValue<Point>(ContainerType.Roaming); }
set { SetValue(value, ContainerType.Roaming); }
}
public int DestinationCount
{
get { return GetValue<int>(10, ContainerType.Roaming); }
set
{
SetValue(value, ContainerType.Roaming);
OnPropertyChanged();
}
}
public string[] DestinationNames
{
get { return GetValue<string[]>(ContainerType.Roaming); }
set
{
SetValue(value, ContainerType.Roaming);
OnPropertyChanged();
}
}
public TravelMethod Method
{
get { return GetValue<TravelMethod>(); }
set { SetValue(value); }
}
public TravelSchedule Schedule
{
get { return GetValue<TravelSchedule>(); }
set { SetValue(value); }
}
}
view raw Settings.cs hosted with ❤ by GitHub
public enum TravelMethod
{
Foot,
Bus,
Train,
Airplane,
}
view raw TravelMethod.cs hosted with ❤ by GitHub
using System;
using System.Runtime.Serialization;
[DataContract]
public class TravelSchedule
{
[DataMember]
public string DestinationName { get; set; }
[DataMember]
public DateTime DepatureTime { get; set; }
}
頻繁に参照されるプロパティでコストが気になる場合はアクセサー内でキャッシュするようにすればいいかと思います。

とくに目新しいことはないですが、一つの定型的方法として。

[修正]

GetValueメソッド中で自作クラスの値がまだ存在しなかった場合の処理を修正。