C#: null れる class と null れない struct いずれの可能性もある object を as<T> したい場合の限界、あるいは C# らしい結論
// これはコンパイルできない(´・ω・`) // object -> T static public T As<T>( object o ) { return o as T; }
// as T できるためには T に class 制約が必要。これはコンパイルできるし、期待動作するかもしれない。 // object(class=nullれる何か) -> T static public T As<T>( object o ) where T: class { return o as T; }
// これは As<T> が期待動作する。 object a = "Я(やー)"; var aa = As<string>( a ); Console.Write( aa );
// これは As<T> をコンパイルできないから期待動作できない。 object b = 4649; var bb = As<int>( b ); // <-- oops! Console.Write( bb );
// これはコンパイルできない(´・ω・`) static public T? AsNullable<T>( object o ) { return o as T?; }
// as T? できるためには T に struct 制約が必要。これはコンパイルできるし、期待動作するかもしれない。 static public T? AsNullable<T>( object o ) where T: struct { return o as T?; }
object b = 4649; var bb = AsNullable<int>( b ); // int? 型になる。 // 暗黙的に int 型のフリが得意な struct をラップした class 的に振る舞う。 Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" ); // null れる bb = null; Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" );
// でも(´・ω・`)… int bbb = bb; // <-- oops! int? は int になれないのでコンパイルエラー。
// 「int? -> int または無かった時の int な補助要員を用意」ならできる int bbb = bb ?? -1;
// つまり…(´・ω・`) これなら object から int とか struct な T へも As<T> できる。一応。 static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// 一応、期待動作する。 object b = 4649; int bbb = As( b, -1 ); // 型パラメーターは第2実引数から推論できるので省略させられる
// つまり… 2つオーバーロードしておいて… // 対 class 用 As<T> static public T As<T>( object o ) where T : class { return o as T; } // 対 struct 用 As<T> static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// int, float, double, string; string は class, そのほかは struct var os = new object[]{ 123, 1.23, 12.3f, "hoge" }; // 一応、 どちらにも As<T> できないこともない…(´・ω・`) foreach ( var o in os ) Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As<string>( o )}" );
// こんなんが出る。期待動作といえる場合もあるけど、言えない場合もある(´・ω・`) // 型判定が厳密過ぎて As というより Is Then って感じだね(´・ω・`) 123, -2, -3, -1, 1.23, -3, -1, -2, 12.3, -1, -2, -3, hoge
で、この As<T>
は As<T>
を使う実装者が object
の中身が class
か struct
か知っていないと使えない。そういうわけで「一応」が抜けない(´・ω・`)
// これはダメ、コンパイルエラー。C#のメソッドのシグニチャーに where 句は含まれないので同じシグニチャーのメソッドの重複定義。 static public T As<T>( object o, T default_value ) where T : class { return o as T ?? default_value; } static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// これは期待動作しちゃう。シグニチャーは異なる事になってコンパイルできるし、さっきの os と As<T> の使い方なら期待動作する。 static public T As<T>( object o, object default_value = null ) where T : class { return o as T ?? default_value as T; } static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// でも、こうすると死ぬので「期待動作しちゃう」だった。 var os = new object[]{ 123, 1.23, 12.3f, "hoge" }; foreach ( var o in os ) Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As( o, "fuga" )}" ); // <-- 最後の As も書き方を統一してみちゃうと死ぬ。
As<T>( o, "fuga" )
は class
制約の方が呼ばれて欲しいんだけど、そんなプログラマーのお気持ちは言語仕様は組んでゆるふわ動作してくれないから、死ぬ。
// あーれこれと考えた結果、たどり着いた C# の限界に無理をしない As<T> たち static public T AsClass<T>( object o ) where T : class { return o as T; } static public T? AsStruct<T>( object o ) where T : struct { return o as T?; }
// ついでにおまけ、(double)34.56 を (int)35 に As りたいとかそーゆー場合用 static public T AsConvertible<T>( object o ) where T : IConvertible { return (T)Convert.ChangeType( o, typeof(T) ); }
まあ…、一応考えてはみましたよ、内部的には AsClass<T>
も AsStruct<T>
も試した結果を (TC,TS?)
なタプルで返して…とか、 class X<TC,TS>
な感じで class
な object
と struct?
な object
をプロパティーで保持しつつ、 operator
群, Equals
, GetHashCode
, GetType
をよしなに実装した薄いラッパーを As<T>
で返させて…とか、いっそ実引数で型パラメーター相当の…とか。
でも、 C# の言語仕様の限界をそんな疲れて保守性も悪そうな何かを必死に作ってどうにかせんでも…と思い、そもそもそうなると↑の AsClass
, AsStruct
, AsConvertible
も何かの部品に internal
で使うわけでなければ、ほとんど存在価値がないどころか無駄なわけで…。
// 実行時に謎の object をあれかこれかそれか…って処理したいなら(´・ω・`) void f( object o ) { switch ( o ) { case IConvertible convertible: // ... 略 case MyStructX sx: // ... 略 case MyClass1 c1: // ... 略 case MyClass2 c2: // ... 略 } }
メソッドでディスパッチしたり包括的に処理するかっこいい何かとかを考えたりするよりも、実際問題、これが一番書くのも保守するのもらく😂
C#, WPF, XAML: System.Windows.Controls.Control が "いわゆるコントロール" の共通基底だと思い込んで実装したら TextBlock であっさり null 例外に殺された件
// 前提、 C#-7.0 using System.Windows.Controls;
var c = new Control(); if ( c is Label l ) Console.WriteLine( $"Label.Content={l.Content}" ); if ( c is TextBlock t ) // <--ここでコンパイルエラーが生じてくれる。 Console.WriteLine( $"TextBlock.Text={t.Text}" );
c is TextBlock t
に対し error CS0584: Internal compiler error: constant in type pattern matching
が発生し、このコードはコンパイルできません。
てっきり「 XAML で使えるような部品としての意味での "いわゆるコントロール" は Control
を共通の基底型に派生している」と思い込んでいました。実際には以下の通り:
- System.Windows.Threading.DispatcherObject
TextBlock
は Control
を経由せずに Control
の基底にもなっている FrameworkElement
から派生していたんですね😂
最初のコード例のような実装ではコンパイラーがエラーを出してくれるのでプログラマーはどんなに間抜けでも実装をリリースできないので安心、見るからにすぐに気づいて修正できる問題に見えます。しかし、これが ItemsControl
のデータテンプレートから展開される "いわゆるコントロール"(上記の通り実際には Control
とは限らない)を扱う実装を書いてみると、この問題に侵された実装をリリースできてしまう事があります。と、いうかありました😅
// "いわゆるコントロール" を Control だと思い込んだ人の書いたやゔぁいライブラリー実装💀 // ItemsControl から index 番目の ContentPresenter を取得 static public ContentPresenter GetContentPresenter ( ItemsControl control, int index ) { return control .ItemContainerGenerator .ContainerFromIndex( index ) as ContentPresenter ; } // ContentPresenter で Name の "いわゆるコントロール" を取得 static public T FindControlAs<T> ( ContentPresenter presenter, string name ) where T : Control { return presenter .ContentTemplate .FindName( name, presenter ) as T ; } // ItemsControl に GetContentPresenter + FindControlAs の複合技で // index 番目の "いわゆるコントロール" を取得 static public T FindControlAs<T> ( ItemsControl control, int index, string name ) where T : Control { if ( GetContentPresenter( control, index ) is ContentPresenter p ) return FindControlAs<T>( p, name ); return null; } // ItemsControl の index 番目のアイテム(≃データテンプレート)から // 複数の Name 群 names にそれぞれ対応した "いわゆるコントロール" を // 取得して Name => "いわゆるコントロール" の辞書にしてくれる便利機能さん static public IDictionary<string, T> FindControlsAs<T> ( ItemsControl control, int index, params string[] names ) where T : Control { if ( GetContentPresenter( control, index ) is ContentPresenter p ) { return ( from name in names select FindControlAs<T>( p, name ) ).ToDictionary( c => c.Name ); } return new Dictionary<string, T>(); }
とか ライブラリー実装しちゃう わけですよ😅
この実装では ItemsControl
の index
番目の要素から name
な Name
を持つ要素を探せなかった場合は null
を想定していますが、「 ItemsControl
から取り出せたアイテムは Control
を共通基底型として扱えるハズ」という実装になっているため、実際には Control
を基底型としない TextBox
などが ItemsControl
の DataTemplate
にある場合にその取得を試みると、アイテムは取得できるのに Control
にキャストできないため as
キャストで null
が保持され、その null
へ Name
プロパティーを参照しようとして null
例外落ちする、というような現象が起こります。例えば↑のコード例の終端側から3行前の ToDictionary( c => c.Name )
で例外吐くとか。
ItemsControl
の DataTemplate
の持つ要素に至るまではこの実装形態でのライブラリー側ではコンパイル時にプログラマーが「Control
を共通の基底型として使えるハズ」と思い込んでいる状態ではコンパイルエラーやワーニングを吐くように実装できませんから、このライブラリーは使用者の実行時に意図しない例外を吐く上に、使い方によっては「どうしてそこで null
例外が飛んでるの😂」なんて思ってしまうような例外の吐かれ方にもなりえます。
特にライブラリーのリリースビルドだけを使うユーザーにとっては null
例外で落ちる部位で参照しているように見えるオブジェクトは非 null
なために "ライブラリーを疑う" 選択肢に気づけないと悩みスパイラルデスマーチにリソースと精神を浪費してしまうかもしれない。しかも出る情報は NullReferenceException
だけになったりもする:
思い込み、こわいですね😅
今回の ItemsControl
の使い方の想定では、 "いわゆるコントロール" は先の派生ツリーでも示した UIElement
です。実際問題として Label
と TextBox
を扱うだけなら FrameworkElement
でも意図通りに扱えますが、 ItemsControl
のデータテンプレートから ContentPresenter
を経由して Name
で取得可能な要素は UIElement
です。 FrameworkElement
は UIElement
に [RuntimeNameProperty( "Name" )]
ほか "いわゆるコントロール" っぽいプロパティーを付けたりした "事実上の共通の基底型" です。必要に応じて as
りましょう。
おわりに、だいじなこと、もう1回書いとこ。
思い込み、こわいですね😅
( ソフトウェアエンジニアにとって、 null
よりも数段、よっぽどこわいものもある。プログラマーの思い込みによる実装は確実にそのうちの1柱だろう。特にコンパイルエラーやコンパイルワーニングをすり抜けてしまうと問題が起こるまでプログラマーは思い込みの上で安心しきり警戒すらしていないかもしれない。)
C#-7.0: 滲み出すスコープ
Visual Studio の C# プロジェクトでも Error
も Warning
も1件も見逃さない綺麗なビルドを心掛けていますが、 Messages
は見落としていました。何件か来ている。
まず、一番上を見る。 IDE0018: Variable declaration can be inlined
とやら。
C#-7.0 で何がどう変わるかを眺めた際に「うへー」って気持ちになった事があったのを思い出してしまいました。
// C# < 7.0 { var a = "1.23"; double b; // <-- 事前に宣言が必要 var c = double.TryParse( a, out b ); Console.WriteLine( $"{a} {b} {c}" ); } // C# >= 7.0 { var a = "2.34"; //double b; <-- 要らんくなった var c = double.TryParse( a, out double b ); // <-- out の後で変数宣言していた事にできる言語機能 Console.WriteLine( $"{a} {b} {c}" ); }
↑手間がかからず一見便利に見えますが、「スコープが滲み出す」とタイトルに書いたような現象が起こるようになります↓
var a = "3.45"; if ( double.TryParse( a, out double b ) ) Console.WriteLine( $"X: {a} {b}" ); // <-- ここで b が生きているのは感覚的にもまあわかる Console.WriteLine( $"Y: {a} {b}" ); // <-- コンパイル可能、実行可能、 b はここでも生きている
きもぃ。便利さと拮抗する程度には十分にきもぃ😅
この言語仕様上のスコープがプログラマーのソースコード・リーディングにおける視認のスコープ感と明らかに異なるだろう挙動には注意が必要になる。
var p = "3.45"; var q = "4.56"; if ( double.TryParse( p, out double buffer ) ) Console.WriteLine( buffer ); if ( double.TryParse( q, out double buffer ) ) // <-- コンパイル不能 Console.WriteLine( buffer );
プログラマー、特に C 系に近い言語を主戦場とするプログラマーにとって、このコードはコンパイル可能な気がすると思います。しかし、 C#-7.0 言語仕様では "正しく" コンパイル不能なコードです。 p
のパースの際に out
に続けて宣言した double buffer
のスコープは if
と同じスコープになります。 if
の内部でだけ使えるような見栄えのソースコードですが、 if
と同じスコープへと double buffer
変数のスコープは "染み出し" しています。
同様の現象は is
でも発生します。
var n = new List< int >(){ 1,2,3 }; var m = Enumerable.Range( 10, 20 ); if ( n is IEnumerable< int > integers ) Console.WriteLine( integers.Count() ); if ( m is IEnumerable< int > integers ) // <-- コンパイル不能 Console.WriteLine( integers.Count() );
染み出しスコープ言語機能…きもぃ😂 便利なのはワカルけど、どうして if スコープへ束縛とか考えなかったんだろう、言語仕様作った人。
おまけ
他にも Messages
は出ているので一応おまけメモ。
IDE0033 Prefer explicitly provided tuple element name
↑は↓のようなタプルに名前が付けられる場合にもタプル標準の Item1
とか使っていると出る。
var vs = new List< string >(){ "aaa", "bbb", "ccc" }; // ↓の t.Item1 に対して IDE0033 が発生する var xs = ( from v in vs select ( v, v.GetHashCode() ) ).ToDictionary( t => t.Item1, t => t.Item2 ); Console.Write( string.Join( "\n", xs ) );
しかし、このしれっとタプルの変数名を勝手に変更してくれちゃう機能は C#-7.1 かららしい。 .net Framework 4.7.2 とかのプロジェクトでは使えない。 .net Core 2.0 ≃ C# 7.1 らしい。 IDE のおすすめメッセージは良い機能とは思うものの、プロジェクトが使用可能な言語バージョンを超えるサジェストはどうかと思う😂
IDE0042 Test C# Variable declaration can be deconstructed
↑は↓のようなタプルを構造化束縛せずに使っている場合に発生する。
var vs = new ( string uni, int tako )[] { ( "きたむらさき", 123 ) , ( "えぞばふん", 234 ) , ( "あか", 345 ) }; // ↓タプルを構造化束縛せずに使うと IDE0042 foreach ( var t in patterns ) Console.WriteLine( $"uni={t.uni} tako={t.tako}" ); // ↓構造化束縛して使うと出ない foreach ( var ( uni, tako ) in patterns ) Console.WriteLine( $"uni={uni} tako={tako}" );
WPF,XAML: Image に Source で入れると極端にぼやけたり何かがおかしい JPEG に遭遇してしまった時の簡単な対処方法
仕事で扱ってもいるので国土地理院の地理院地図のタイル画像データはよく使わせて頂いています。います、のですが、これがたまにメタデータだったりピクセルフォーマットだったりが奇妙な画像ファイルに遭遇する事がたびたびあります😅
今回は地理院地図のJPEGファイルの一部を WPF の Image コントロールで読ませると極端にぼやけてしまうという怪奇現象に遭遇しました。
- https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg (地理院地図、札幌市中央区の大通公園の映る一角の写真地図)
↑一見ふつーのJPEGファイルのようにウェブブラウザーやほとんどの画像ビューアー等では表示できるのですが…
<!-- 何かがおかしい JPEG をソースに入れると何かがおかしいレンダリング結果が得られてしまう --> <Image Height="60" Width="256" RenderOptions.BitmapScalingMode="Fant" Stretch="None" Source="https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg"/>
↑これが実行すると↓こんな表示になってしまう😂
↑この JPEG ファイルのバイナリー↓からメタデータを解析すると
- 0x0D ( 1 byte ) = 0x01 ( "Density units", 0x01 = "Pixels per inch" )
- 0x0E ( 2 bytes; littile-endian ) = 0x0001 ( "Xdensity"; Horizontal pixel density )
- 0x10 ( 2 bytes; littile-endian ) = 0x0001 ( "Ydensity"; Horizontal pixel density )
と、なっていました。「ピクセルの密度単位が 1 pixels/in 」という事は、「1 ピクセルにつき 1 in = 2.54 cm に展開」という事なので、地理院地図のタイル 256x256 pixels は「 6.5 m x 6.5m の表示サイズに展開されるのが正しい」というメタデータになっているのです。なるほど真面目にメタデータを読んじゃったら極端にぼやけるよね(´・ω・`)
<!-- とりあえず簡単な対処方法 --> <Image Height="60" Width="256" RenderOptions.BitmapScalingMode="Fant" Stretch="UniformToFill" Source="https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg"/>
ドットバイドットを期待して Stretch
を None
にしたくなるお気持ちを抑えて UniformToFill
を使います。(あるいは Image
の Width
と Height
の縦横比が画像と同じなら Fill
でも期待する表示にできるでしょう😃)
↑XAMLの属性だけで簡単にどうにかできました😋
XAMLではなくソースコード書くぞー的に対応したい場合は、ソース取得時のストリームに介入するか、デコーダーに介入するか、 BitmapImage
でデコード後の画像データの分解能情報を妥当な値へ修正するか、ピクセル配列をさらって適当なリソース型へ複製しちゃうなどお好みのレベルで対応できると思います。が、今回は XAML でプロパティー値をちょっと気にするだけで対応できたのでそういうはなしは無しで。
ちなみに、一般的にはパソコンやスマフォなどの一般的なディスプレイ向けのピクセル密度、画像分解能は 96 pixels/in ( DPI=dots/in でもおなじ )に設定されています(特に Windows では長いこと 96 DPI 基準が染み付いています)。 そうしなければならない、という決まりがあるわけではない慣習による値ですが、ヘッダーをてきとーに作ってしまうと今回のように画像ファイルを使用するユーザー側で思わぬトラブルや扱いに手間暇の必要な事態になってしまう事がたまによく起こります😂
96 DPI = 0.01041666666 in/pixel = 0.02645833333 cm/pixel での 256 pixels は 6.77333333333 cm となります。まあ、そんなもんかな、という大きさの感覚で見られるように思います。少なくとも縦横 6.5m はいくらなんでもデータ生成の誤りでしょう。
のちほど、本件は適当な方法で地理院タイルのなかのひとに伝わるように報告しておこうと思います。以前、規則性不明にタイルの一部のPNGファイルが16-Bitピクセルフォーマットで提供されているためにデコーダー、ライブラリーによっては対応できない事がある問題を地理院パートナーネットワークのアンケートで報告した際には数カ月後にしれっと対応して頂けていたし、 HTTP/2 対応なども要望したらいつの間にかしれっと対応、タイルデータの配布も高速にできるよう何かと工夫を進めてくれているので、本件も適当に報告しておけばいつの間にかどうにかしてくれるのではないかな、と期待しています😋
References
C#/.net/NuGet: nuget デビューしたはなし
- usagi.cs ; わたしが欲しいと思った適当な機能たち for C#
数年ぶりに C# の実装を書いていて「きぶん」と「おもいつき」で書いてみたライブラリー的な。そのうち整理もするけれど、とりあえず今週末は nuget とかいう何かの author としての使い方の基本を覚える事にしたのです。
Microsoft の解説は、
- NuGet パッケージの公開方法 | Microsoft Docs
- Create and publish a .NET Standard package using Visual Studio on Windows | Microsoft Docs
- 次にここを見て「うちのプロジェクト、 Package タブないやん(´・ω・`)」
- Create and publish a .NET Framework package using Visual Studio on Windows | Microsoft Docs
- 次にここを見て「ほいほい、ほな nuget ... コマンドどこじゃ…パスも通ってないしNATIVEのCMDでもパス通ってないし、いつものところに入ってそうだけど探すのめんどくさー」
- NuGet Gallery | Downloads
…と、うろうろしながら nuget パッケージを作って nuget.org で owner として公開しました。
手順メモ:
- VS2017 Installer で NuGet の…と、いうのは実際問題どうでもよいので気にしない事にして IDE とかいんてぐれーてっどの事は忘れる。
- https://www.nuget.org/downloads で最新の Windows x86 Commandline を拾ってくる。ナマ・エグゼをダウンロードするドキドキ体験♥
- nuget.exe をどこか適当なところに置いて、端末エミュレーター的な何かから nuget.exe を叩いてサブコマンドリストが出る動作を確認。
- 私はどうせ Visual Studio で扱う
%HOMEPATH%/source/repos/
以下の dir の .sln の下の .csproj でしか使わないので、このrepos
にとりあえずで置いた。
- 私はどうせ Visual Studio で扱う
nuget.exe spec myproject.csproj
してmyproject.nuspec
を生成myproject.nuspec
を適当に編集- 先の Microsoft の案内によれば
licenseUrl
,projectUrl
,iconUrl
,releaseNotes
,tags
を適当に書いてね、との事なので書いた。- 先に repos を github へ公開しておいたので
licenseUrl
,projectUrl
はそこへリンクしただけにした。(ただし、どうもlicenseUrl
をパッケージ外のURLにするのは nuget.org 的には Deprecated ではあるらしい。) iconUrl
は Gravater の私のいつものアイコンの URL にしておいた。
- 先に repos を github へ公開しておいたので
- 加えて、
authors
,owners
は$author$
のままだと nuget.exe がエラーで死ぬので、適当に変数ではない値を書いておく必要があった。
- 先の Microsoft の案内によれば
nuget.exe pack
してmyproject.x.y.z.nupkg
をこさえる(myproject.nuspec
があるディレクトリーで行う)- ウェブブラウザーで https://www.nuget.org/packages/manage/upload から
myproject.x.y.z.nupkg
をアップロードする - README を URL から回収するユーザーインターフェースがあるので github の README.md の raw な URL など貼って取り込ませる。
↓ちなみに・w・
↑nuget.exe
は WSL の zsh から叩いても動きます(゜∀。)
UE4/C++: USoundWave と USoundCue または何れにせよ USoundBase でいいやのメモ
UE4 プロジェクト内のアセット化された「なにか」を C++ コードの ctor で拾いたい場合には以下のようにする、するのだが…:
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
// どこかの UObject 系の ctor でプロジェクト内のアセットを引っ張り出す(間違い探しアリ😋) static ConstructorHelpers::FObjectFinder< USoundCue > my_sound ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) );
この実装はコンパイル可能で my_sound
に対して特に何も実装を加えていなければうっかり見過ごしてしまうかもしれない。しかし、 UE4 のビルドシステムのログにはしっかりとエラーが報告されている。(少なくとも UE4Editor では reload に失敗するのでほぼ確実に気付くと思う。)
Error: CDO Constructor (MyExperimentalActor): Failed to find SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'
game_maoudamashii_3_theme01 はゲーム系フリー音楽素材の配信元としておなじみの 魔王魂 で入手できる。今回はこの .wav
が /Game/BGMs
に入っていなかった落ちではない。
今回の「間違い」は USoundCue
として .wav
アセットを探すコードになっている事。 .wav
アセットは UE4 的には USoundWave
なので:
// .wav を放り込んだだけの USoundWave アセットに対する正しい実装例 static ConstructorHelpers::FObjectFinder< USoundWave > my_sound ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) );
と、しなければ期待動作しない。これが UE4Editor で放り込んだ .wav
のコンテキストメニューから Create Cue
するなどして USoundCue
化されたアセットを作ってあるのなら:
// USoundCue に対する正しい実装例 static ConstructorHelpers::FObjectFinder< USoundCue> my_sound ( TEXT( "SoundCue'/Game/BGMs/game_maoudamashii_3_theme01_Cue.game_maoudamashii_3_theme01_Cue'" ) );
となる。
なるのだが、実際問題この段階で USoundCue
か USoundWave
か何れであるかが問題になる事はたぶん無い。面倒だ・w・
そこで、とりあえず区別する必要の無い段階では USoundBase
を使っておけばよい。 USoundBase
は USoundCue
と USoundWave
の何れの基底型でもある:
// USoundCue でも USoundWave でも USoundBase にしておけばどっちでもいいのです static ConstructorHelpers::FObjectFinder< USoundBase > my_sound_a ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) ); static ConstructorHelpers::FObjectFinder< USoundBase > my_sound_b ( TEXT( "SoundCue'/Game/BGMs/game_maoudamashii_3_theme01_Cue.game_maoudamashii_3_theme01_Cue'" ) );
こうして作った USoundBase
は「どちらでもいいや」のまま UAudioComponent::SetSound
のパラメーターとして与えられる。(そもそも SetSound
の第1引数は USoundBase
型。)
// どこかに適当な UAudioComponent が居るとして auto c = NewObject< UAudioComponent >( this ); c->SetSound( my_sound_a.Object );
USoundCue
と USoundWave
、こやつらの違いは必要な事があれば Cast
すればいいし、トリアエズナラスダケ!くらいな程度では USoundBase
で取り回していても不便ないしね☺
References
C#: ビルド番号の自動採番の有効化と Properties.Settings.Default の Upgrade トリックの必要について
遠いむかしの記憶、 C# というか Visual Studio のプロジェクト管理システムにはバージョン番号の一部を自動採番する便利な機能があった、ような気がした。実際に執筆時点で現行製品の VS2017 で作った C# のプロジェクトの AssemblyInfo.cs
にも次のコメントが自動的に書かれた状態で生成されていた。
// You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")]
ビルド番号を自動採番にしておくと頻繁な開発とα版、β版の試供などを行うプロジェクトでは何かと便利が良い。そこで、
// 実際に現行の VS2017 で C# プロジェクトを作成し、 AssemblyInfo.cs を // コメントに従って自動採番ひゃっほーぃ!してみるのだが… [assembly: AssemblyVersion("0.0.0.*")]
このようなワイルドカードバージョンを定義すると VS2017 は自動生成されていたコメントとは裏腹に 嫌な気配のする赤波線
を引いてエラーを報告し、プロジェクトはビルド不能に陥ってしまう。
Error CS8357 The specified version string contains wildcards, which are not compatible with determinism. Either remove wildcards from the version string, or disable determinism for this compilation
この問題はググるとナウい C#er たちが解決方法を教えてくれていた😋
- AssemblyVersionでワイルドカードを使用するとエラーになる。-Using wildcards with AssemblyVersion results in an error. - Developer Community
- エラー CS8357 指定されたバージョン文字列にワイルドカードが含まれていますが、これは決定性と相容れません。バージョン文字列からワイルドカードを削除するか、このコンパイルで決定性を無効にします - Qiita
<!-- your-project.csproj を適当なテキストエディターで直接開いていじる --> <Project ... 略 ... > <PropertyGroup> ... 略 ... <!-- ↓これを true から false へ変える --> <Deterministic>false</Deterministic>
さて、それでバージョン番号に自動採番を有効化してみると、今度はソースの微小な変更であれ再ビルドが発生するとバージョン番号が変わるため、 Properties.Settings.Default
によりユーザーごとに保存されるはずの設定値がすべて再ビルドを挟む度にプロジェクトの Settings.settings
の初期値へと毎度戻ってしまう問題に遭遇した。
この問題の解決方法は stackoverflow でベテラン C#er の Markus Olsson がベストな答えを投稿してくれていた:
Settings.settinfs
へUpgradeRequired
をbool
で追加し、既定値をTrue
にしておく。- 設定値をアプリが使う前(例えばメインの
Window
クラスの構築子とか)にUpgradeRequired
をチェックしてUpgrade
メソッドを呼んでSave
もしてしまう。
なるほど😃
なお、この設定値の問題は何れしばらく後にリリース版のアプリのバージョン番号がアップグレードされる際にも発生したであろうので、さっさと気づけて良かったとも思える。