- 投稿日:2019-05-29T23:44:21+09:00
C#のリストで、条件に一致するインデックスを取得する方法
こんにちは、ブログ「学生ブロックチェーンエンジニアのブログ」を運営しているアカネヤ(@ToshioAkaneya)です。
C#のリストで、条件に一致するインデックスを取得する方法
この目的には
FindIndex
が使えます。例
sampleList
から、idが3の要素のインデックスを取得するには次のようにします。var index = sampleList.FindIndex(sample => sample.id == 3);その他の方法についてコメントもお待ちしております。
この記事が参考になれば幸いです。はてなブックマーク・Pocketはこちらから
- 投稿日:2019-05-29T17:28:45+09:00
一部のHTMLタグだけ許可する(正規表現編)
はじめに
これは一部のHTMLタグだけ許可する(DOM編)の続編で、正規表現で、一部のHTMLタグを許可してみようという試みである。
アーキテクチャ(基本方針)
こんな感じ
- 全体をHTMLエンコードしておく
- 許可タグだけ、HTMLデコードで、元のタグに戻していく
許可タグとして認識できる文字列だけ、ホワイトリスト方式のように、元のタグに戻るので、想定していないタグが有効になるなどの予想もしていない脆弱性が生まれないだろう。
アーキテクチャ(基本方針)(あいまいな書式の HTML)
前回の「一部のHTMLタグだけ許可する(DOM編)」もそうだが、Webページ上のリッチテキストライブラリが吐き出すHTML形式の装飾されたテキストをXSSのような脆弱性を生むことなく利用しよう、というのが前提なので、自由度のあるあいまいな解釈(例えば、ダブルクォートだけではなくシングルクォートで括ってもよいとか、そもそも括らなくてもなんとなく解釈してくれるとか)をする必要はなくて、そのWebページ上のリッチテキストライブラリが吐き出す形式だけ許可するようにすればよい。
アーキテクチャ(基本方針)(事前のHTMLエンコード)
入力データは、HTMLエンコードがある程度実施されているという前提(<と<の混在)だと仮定すると、単純な HTMLエンコード処理ではなく、「<」「>」「"」だけをエンコードする方がよい場合(無用な二重エンコードは汚い)もあるので、このあたりは入力されるデータの書式をあらかじめ吟味しておく必要がある。
まぁ、一旦HTMLエンコードしたうえで、「&amp;」→「&」に置換しておく。という方法で無用な二重エンコードを防いでもよい。
アーキテクチャ(基本方針)(属性のないタグ)
前篇からの前提を引き継いで、属性のない許可タグとして B タグと、Iタグと、それと、今回は BR タグを追加しよう。
属性がないので、正規表現としては単純だ。
つまり
- <B>
- </B>
- <I>
- </i>
- <BR>
だ。
データは事前にHTMLエンコードされているので、これらは、
- <B>
- </B>
- <I>
- </i>
- <BR>
になる。それぞれ、これらをHTMLデコードしてもどしてやるだけなので、特に問題はないだろう。
これらを一つの正規表現にまとめると、
<((/?((b)|(i)))|(br)|(/font))>
(ついでにfontの閉じタグも仲間に入れたよ)
アーキテクチャ(基本方針)(許可属性のあるタグ)
ここも前篇からの前提を引き継いで、FONTタグで、許可属性は、sizeとcolorということにしてみる。
正規表現は、こんな感じになるだろう。
<font((●●●●)|(◆◆◆◆))+>
アーキテクチャ(基本方針)(許可属性のあるタグ)(size属性)
さて、一つ目の●●●●について、size属性について考えてみよう。
size属性は、数値なので、数値しか与えられないだろう。
という事で、こんな感じになるsize="[0-9]+"
属性の値についてもホワイトリスト方式のように許可のみヒットするようにする事が重要
アーキテクチャ(基本方針)(許可属性のあるタグ)(color属性)
color属性は、アルファベット(色名)と#16進数なので、こんな感じでどうだろう。
color="[a-z0-9#]+"
特に、属性の値として「"」や「'」(エンコードされた「"」と「'」)は危険なので、それらがヒットしないように注意
アーキテクチャ(基本方針)(許可属性のあるタグ)(まとめ)
上記をまとめるとこんな感じになる
<font(( size="[0-9]+")|( color="[a-z0-9#]+"))+>
テスト結果
サンプルコードについて
サンプルはC#/.NET Frameworkだけど、JavaとかでPHPには移植しやすいと思う。
サンプルは、Bタグと、Iタグと、FONTタグのCOLORとSIZE属性だけ許可するようにしている正規表現だが、それ以外のタグについては、これらの正規表現を修正すればいいだろう。
DOM版よりコードが短くなっているなぁ~...
サンプルコードについて(正規表現のクラス)
正規表現のオプション(System.Text.RegularExpressions.RegexOptions)は、サンプルコードでは RegexOptions.IgnoreCase|RegexOptions.Singleline だけど、アプリケーション起動中に解放しないという事であれば RegexOptions.Compiled を足すと処理が速くなるそうだ。
(なんか、Compiled にすると、解放時にメモリリークみたいな問題があるそうだけど、逆に言えば解放しない固定な正規表現には使えるという事だと思う)私はstaticのRegexにしている
public static Regex obj = new RegEx(......
みたいな感じ
サンプルコード(Program.cs)
using System; using System.IO; using System.Text.RegularExpressions; using System.Web; namespace StripHTMLs { class Program { static void Main(string[] args) { StreamReader reader = new StreamReader(new FileStream(args[0], FileMode.Open, FileAccess.Read)); String motoData = reader.ReadToEnd(); Console.WriteLine("FileName: " + args[0]); Console.WriteLine("Input Data is: "); Console.WriteLine(motoData); Console.WriteLine("========================"); Console.WriteLine("ans is"); // まずは、HTMLエンコードをする(既にエンコード済の文字が再エンコードされないように「<」「>」「"」だけエンコードする) motoData = motoData.Replace("<","<").Replace(">",">").Replace("\"","""); // <b></b><i></i><br></font>など属性のない許可タグ(と閉じタグ)を元に戻す // <(((/)?((b)|(i)))|(br)|(/font))> // → // < // ( // ( // (/)?((b)|(i)) // ) // | // (br) // | // (/font) // ) // > Regex regex0 = new Regex("<(((/)?((b)|(i)))|(br)|(/font))>", RegexOptions.IgnoreCase | RegexOptions.Singleline); motoData = regex0.Replace(motoData, m => HttpUtility.HtmlDecode(m.Value) ); // fontタグのsizeとcolorだけ許可する // <font(( size="[0-9]+")|( color="[a-z0-9#]+"))+> // → // <font // ( // ( size="[0-9]+") // | // ( color="[a-z0-9#]+") // )+ // > Regex regex1 = new Regex("<font(( size="[0-9]+")|( color="[a-z0-9#]+"))+>", RegexOptions.IgnoreCase | RegexOptions.Singleline); motoData = regex1.Replace(motoData, m => HttpUtility.HtmlDecode(m.Value)); Console.WriteLine(motoData); } } }以上
- 投稿日:2019-05-29T15:00:13+09:00
VisualStudio2017 で SalesForce の SOAP API を叩く(Partner版)
やりたいこと
C# で SOAPAPI を利用して SalesForce 上のオブジェクトを操作
公式ドキュメントはこちらPartner 版
- 弱い型付け
- typo に弱い
- カスタムオブジェクト、項目に左右されない
Enterprise 版の記事はこちら
事前準備
Enterprise版とほぼ同じだけど
WSDL は Partner 版を選択実装
ログイン/ログアウト
Enterprise版と同じ
オブジェクト操作
例として Contact の操作
Read.cs/// <summary> /// Contact取得 /// </summary> private void getContacts() { // SQL発行 String soqlQuery = "SELECT Id, FirstName, LastName FROM Contact"; try { // デフォルトだと500件までしか取れない QueryResult qr = binding.query(soqlQuery); string msg = ""; while (true) { sObject[] records = qr.records; for (int i = 0; i < records.Length; i++) { sObject con = records[i]; // Idはどのデータにも存在する // あとはSELECT時のカラム番号で参照 msg += con.Id + ":" + con.Any[2].InnerText + " " + con.Any[1].InnerText + "\r\n"; } if (qr.done) { // おわり break; } else { // 500件以上存在する場合、次の500件を取得 qr = binding.queryMore(qr.queryLocator); } } MessageBox.Show(msg); } catch (Exception ex) { // エラー MessageBox.Show(ex.Message); } }Upsert.cs/// <summary> /// Contact更新 /// </summary> public void upsertContacts() { // 更新対象配列作成(テストなので適当) sObject[] upserts = new sObject[1]; sObject c0 = new sObject(); c0.type = "Contact"; // オブジェクト指定 // XML作成 System.Xml.XmlElement[] contactFields = new System.Xml.XmlElement[4]; System.Xml.XmlDocument doc = new System.Xml.XmlDocument(); // 氏 contactFields[0] = doc.CreateElement("LastName"); contactFields[0].InnerText = "てすと"; // 名 contactFields[1] = doc.CreateElement("FirstName"); contactFields[1].InnerText = "たろう"; // Email contactFields[2] = doc.CreateElement("Email"); contactFields[2].InnerText = "test@example.com"; // 勤務先 contactFields[3] = doc.CreateElement("AccountId"); contactFields[3].InnerText = "001000000000000"; c0.Any = contactFields; upserts[0] = c0; try { // Email一致の場合UPDATE、それ以外の場合INSERT UpsertResult[] upsertResults = binding.upsert("Email", upserts); string msg = ""; foreach (UpsertResult result in upsertResults) { if (result.success) { msg += result.id + " : " + (result.created ? "Insert" : "Update") + "\r\n"; } else { MessageBox.Show("Error!: " + result.errors[0].message); } } MessageBox.Show(msg); } catch (SoapException e) { MessageBox.Show(e.Message); } }おわりに
SalesForce のオブジェクトいじっても WSDL 再生成しなくていいから楽!
でも型がないのちょっとこわい
- 投稿日:2019-05-29T14:56:06+09:00
物体追従の処理パフォーマンスを上げる
問題とすること
opencv_contrib には物体追従(トラッキング)をしてくれる trackingモジュール という素晴らしいモジュールが用意されています。しかし、このトラッキング処理はイメージサイズに反比例してパフォーマンスが低下するという、ある意味当たり前ですが、パフォーマンスの問題を抱えています。
時に映像内の物体を追従したいといった時、特にリアルタイム処理したい場合には、この問題は顕著にフレームレートの低下をもたらしてしまうことがあります。この記事はその問題に対する1つの回答です。
解決方法は?
アイディアは至って簡単です。大きなイメージサイズを扱わず、小さなイメージにリサイズしてからトラッキングを行います。リサイズ処理のコストはトラッキング処理のコストよりも遥かに小さく済みます。
パフォーマンス改善の結果
VisualStudioのパフォーマンスプロファイラーを使って、CPU処理を計測してみました。
測定プログラムについて
OpenCvSharpを使用してカメラ入力の映像をリアルタイムで処理するWPFアプリケーションを作成しました。カメラ入力映像はHD解像度(1280 x 720)ですが、対応前はそのまま処理しており、対応後は内部的にSD解像度(640 x 480)に変換してから処理をしています。
測定方法
トラッキング処理のコアメソッドである OpenCvSharp.Tarcking.Update のCPU占有率を比較しました。計測機器はLenovo Thinkpad T470s (CPU Core i7-7500U @ 2.7Ghz)を使っています。
結果
対応前
対応後
大雑把ですが、結果として、トラッキング処理に使われているCPUが、2/3になりました。
おまけ
参考までに計測に使用したコードの一部を加工して載せます。OpenCvSharpを使う人なら普通に実装できると思います。
大雑把な解説ですが、カメラ映像等の取得ループ内で使う前提で、トラッキング領域ごとにnewして、Initでトラッキングしたい領域を指定、イメージが変わるたびにUpdateを呼び出して追従処理をする感じです。マジックナンバーになっていますが、内部的には幅640px超のイメージはアスペクト比を保ったまま640pxにリサイズして処理します。
SmartTracker.csusing OpenCvSharp; using OpenCvSharp.Tracking; namespace MyApp { public class SmartTracker { private Tracker _tracker; private bool _tarcking = false; private Rect2d _boundingBox; private Rect? _initRect; private double _imageRatio = 1.0; private const double _MAX_IMAGE_WIDTH = 640.0; public SmartTracker() { _tracker = TrackerMedianFlow.Create(); } public bool Init(Mat image, Rect target) { // トラッキングの処理効率を上げるために内部的には最大幅640pxのイメージとして処理する // この時、内部の_initRect, _boundingBox等は縮小したイメージで処理されるので、 // publicメソッド内で復元して呼び出し元に返却する if (image.Width > _MAX_IMAGE_WIDTH) { _imageRatio = _MAX_IMAGE_WIDTH / image.Width; image = image.Resize(new Size(image.Width * _imageRatio, image.Height * _imageRatio)); target = ConvertInternalSizeRect(target); } else { _imageRatio = 1.0; } _boundingBox = new Rect2d(target.X, target.Y, target.Width, target.Height); _initRect = target; try { _tarcking = _tracker.Init(image, _boundingBox); } catch { _tarcking = false; } return _tarcking; } public Rect? Update(Mat image, Rect? area = null) { if(_initRect == null) { return null; } if (image.Width > _MAX_IMAGE_WIDTH) { _imageRatio = _MAX_IMAGE_WIDTH / image.Width; image = image.Resize(new Size(image.Width * _imageRatio, image.Height * _imageRatio)); if (area.HasValue) { area = ConvertInternalSizeRect((Rect)area); } } else { _imageRatio = 1.0; } Rect retRect; try { _tracker.Update(image, ref _boundingBox); retRect = new Rect((int)(_boundingBox.X), (int)(_boundingBox.Y), (int)(_boundingBox.Width), (int)(_boundingBox.Height)); // 指定エリアの外になったら抜ける if (area.HasValue) { Rect a = (Rect)area; if (!a.Contains(retRect.TopLeft) || !a.Contains(retRect.BottomRight)) { Reset(); return null; } } return retRect; } catch { Reset(); return null; } } public void Reset() { _initRect = null; _tarcking = false; if (!_tracker.IsDisposed) { _tracker.Dispose(); } _tracker = TrackerMedianFlow.Create(); } public bool IsTracking() { return (_initRect.HasValue) ? true : false; } private Rect ConvertOriginalSizeRect(Rect rect) { return new Rect((int)(rect.X / _imageRatio), (int)(rect.Y / _imageRatio), (int)(rect.Width / _imageRatio), (int)(rect.Height / _imageRatio)); } private Rect ConvertInternalSizeRect(Rect rect) { return new Rect((int)(rect.X * _imageRatio), (int)(rect.Y * _imageRatio), (int)(rect.Width * _imageRatio), (int)(rect.Height * _imageRatio)); } } }以上。
OpenCVって使い慣れてくると便利です。OpenCvSharpを作ったshimatさん、感謝しています。皆さんも良いOpenCVライフを送ってください。
- 投稿日:2019-05-29T07:23:36+09:00
Unityでマイクの音を任意の時間だけファイルに書き出しながら録音する
やりたいこと
- Unityでマイクからの入力を録音したい。
- 好きなタイミングで録音を開始して、好きなタイミングで録音を停止したい。
- wavファイルにしたい。
Unity標準の録音機能だとあらかじめ録音時間を指定した固定長でしか録音できません。途中で録音を止めると残り時間に無音データを詰め込まれたものが出てきます。
普通の用途ならこのやり方で必要な長さのAudioClip
を作ってUnityWav1で変換をかければ十分だと思います。自分の場合は以下の条件が追加されたので一捻りする必要がありました。
- 録音時間が不明、長時間も予想される
Unity標準APIでの録音は60分が限界- メモリがキツキツ
音声データをメモリに保持しておけないので随時書き出す必要があるコードと使い方
コード
MicRecorder.csusing System; using System.Collections; using System.IO; using UnityEngine; namespace NekomimiDaimao { /// https://gist.github.com/nekomimi-daimao/a14301d7008d0a1c7e55977d6d9e2cc1 public class MicRecorder : MonoBehaviour { private const int Frequency = 44100; private const int MaxLengthSec = 600; private const int HeaderLength = 44; private const int RescaleFactor = 32767; private FileStream _fileStream; private AudioClip _audioClip; private string _micName = null; private Coroutine _recordingCoroutine; public bool IsRecording { get; private set; } = false; public void StartRecord() { if (IsRecording || _recordingCoroutine != null) { return; } IsRecording = true; _recordingCoroutine = StartCoroutine(StartRecordCoroutine()); } /// <summary> /// yield return StartCoroutine(MicRecorder.StopRecord()); /// </summary> public IEnumerator StopRecord() { IsRecording = false; yield return _recordingCoroutine; _recordingCoroutine = null; } private IEnumerator StartRecordCoroutine(string defaultPath = null) { try { var path = defaultPath ?? $"{Application.temporaryCachePath}/record/{DateTime.Now:MMddHHmmss}.wav"; _fileStream = new FileStream(path, FileMode.Create); const byte emptyByte = new byte(); for (var count = 0; count < HeaderLength; count++) { _fileStream.WriteByte(emptyByte); } if (Microphone.devices.Length == 0) { yield break; } _micName = Microphone.devices[0]; _audioClip = Microphone.Start(_micName, true, MaxLengthSec, Frequency); var buffer = new float[MaxLengthSec * Frequency]; var head = 0; int pos; do { pos = Microphone.GetPosition(_micName); if (pos >= 0 && pos != head) { _audioClip.GetData(buffer, 0); var writeBuffer = CreateWriteBuffer(pos, head, buffer); ConvertAndWrite(writeBuffer); head = pos; } yield return null; } while (IsRecording); pos = Microphone.GetPosition(_micName); if (pos >= 0 && pos != head) { _audioClip.GetData(buffer, 0); var writeBuffer = CreateWriteBuffer(pos, head, buffer); ConvertAndWrite(writeBuffer); } Microphone.End(_micName); WriteWavHeader(_fileStream, _audioClip.channels, Frequency); } finally { _fileStream?.Dispose(); _fileStream = null; AudioClip.Destroy(_audioClip); _audioClip = null; _micName = null; } } private static float[] CreateWriteBuffer(int pos, int head, float[] buffer) { float[] writeBuffer; if (head < pos) { writeBuffer = new float[pos - head]; Array.Copy(buffer, head, writeBuffer, 0, writeBuffer.Length); } else { writeBuffer = new float[(buffer.Length - head) + pos]; Array.Copy(buffer, head, writeBuffer, 0, (buffer.Length - head)); Array.Copy(buffer, 0, writeBuffer, (buffer.Length - head), pos); } return writeBuffer; } private void ConvertAndWrite(float[] dataSource) { Int16[] intData = new Int16[dataSource.Length]; var bytesData = new byte[dataSource.Length * 2]; for (int i = 0; i < dataSource.Length; i++) { intData[i] = (short) (dataSource[i] * RescaleFactor); var byteArr = new byte[2]; byteArr = BitConverter.GetBytes(intData[i]); byteArr.CopyTo(bytesData, i * 2); } _fileStream.Write(bytesData, 0, bytesData.Length); } private void WriteWavHeader(FileStream fileStream, int channels, int samplingFrequency) { var samples = ((int) fileStream.Length - HeaderLength) / 2; fileStream.Flush(); fileStream.Seek(0, SeekOrigin.Begin); Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF"); fileStream.Write(riff, 0, 4); Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8); fileStream.Write(chunkSize, 0, 4); Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE"); fileStream.Write(wave, 0, 4); Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt "); fileStream.Write(fmt, 0, 4); Byte[] subChunk1 = BitConverter.GetBytes(16); fileStream.Write(subChunk1, 0, 4); //UInt16 _two = 2; UInt16 _one = 1; Byte[] audioFormat = BitConverter.GetBytes(_one); fileStream.Write(audioFormat, 0, 2); Byte[] numChannels = BitConverter.GetBytes(channels); fileStream.Write(numChannels, 0, 2); Byte[] sampleRate = BitConverter.GetBytes(samplingFrequency); fileStream.Write(sampleRate, 0, 4); Byte[] byteRate = BitConverter.GetBytes(samplingFrequency * channels * 2); fileStream.Write(byteRate, 0, 4); UInt16 blockAlign = (ushort) (channels * 2); fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2); UInt16 bps = 16; Byte[] bitsPerSample = BitConverter.GetBytes(bps); fileStream.Write(bitsPerSample, 0, 2); Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data"); fileStream.Write(datastring, 0, 4); Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); fileStream.Write(subChunk2, 0, 4); fileStream.Flush(); fileStream.Close(); } } }使い方
// 特に依存しているComponentはないです [SerializeField] private MicRecorder _micRecorder; // 録音開始。これは別にCoroutineではない _micRecorder.StartRecord(); // 録音停止するときは処理をぜんぶ終える必要があるため、Coroutineの終了を待機してあげる yield return StartCoroutine(_micRecorder.StopRecord()); // 録音中フラグだがMicRecorder.StopRecordを実行してから // 完全に終了するまでの間もfalseなのであんま信用してはいけない Debug.Log($"Are you listening? {_micRecorder.IsRecording}");解説と注意事項
探すと理屈を解説した記事は見つかるのですが、何故かコピペしてそのまま使えるやつはなかったので、いろんなところからコピペして切り貼りしました。なんかめっちゃかけ算? してるな? ぐらいの理解度だ!
特にCreateWriteBuffer
でバッファを使い切って最初からになったときの処理。
(バッファのヘッドから最後まで) + (バッファの最初から現在のポジションまで)
ですがちょっとくらい取りこぼしててもバレへんやろの精神です。Enumerable.Range()
とかで配列作れば確認できますがめんどくせえのでしてません。
Frequency
は44100
でしか確認してないので他のサンプリングレートで動くかは未確認。
MaxLengthSec
は600
なので結局10分もメモリを確保してます。状況に合わせて削ってください。理屈の上では10もあれば動くはずです。セルフレビュー
using()使ってない
微妙に使い方がまだよくわかってないやつ。
この手のリソースを自動で閉じておいてくれる記法としてはJava
のtry-with-resources
がいちばん優れてるような気がするんですが、どこも導入してくれなくてつらい。成否がわからない
途中で録音に失敗してもなんらかのゴミファイルは作成されてしまう。なので成否のコールバックが必要だが……。
StopRecord
をTask
に置き換えて成否のbool
を返すとか。
IsRecording
を外部に公開している
内部処理用のフラグと外部公開用のフラグは分割するべき。
MonoBehaviour
を継承している
コルーチンがなければ継承の必要がない。のでできればTask
にしたい。が、UnityのApiはメインスレッドからしか実行できないのでTask
にすると管理がめんどくせえ!メインスレッドでファイルを書き込みしてる
せめてバッファ変換・ファイル書き込みをワーカースレッドに逃したい。別にそんな処理重くないっぽいけど一応。こんなコードが上がってきたらコードレビューで撥ねますが、まあ動いてるしいいかなって思いました。
おしまい。
参考
【Unity】長時間のマイク録音を実現する方法
【Unity】AudioListerを録音してwavにする
UnityのMicrophoneで正確な録音時間を取得する方法
なんか適当なパスにファイル保存しちゃうのなんで? ↩
- 投稿日:2019-05-29T01:07:49+09:00
C# 8.0 の目玉機能「null許容参照型」で既存プログラムを安全にnull安全にする
はじめに
C# 8.0は、本稿執筆時点(2019年5月)では、まだプレビュー段階であり、正式リリースされていませんが、プレビュー段階でも利用できます。
本稿は、C# 8.0の目玉機能「null許容参照型」を用いて既存プログラムを安全にnull安全にする方法の紹介です。
なお、既存プログラムでなく、新規でプログラムを作成する際は、無条件でこの機能を有効にした方が良いと思います。実際、私のプロジェクトでは、新規のプログラムは、必ずこの機能を有効にして開発しています。null許容参照型とは
従来、値型は string? のように末尾に「?」を記載するかどうかでnullの許容と非許容を区別できましたが、参照型はすべてnull許容でした。
C# 8.0でnull許容参照型の機能を有効にすると、参照型に対して T? と書くとnull許容になり、単に T と書くとnullを認めない型になります。// [object]はnullを設定できないため、以下は警告になる object objA = null; // [object?]はnullを設定可能なため、以下は警告にならない object? objB = null;nullに設定できない型に、nullを設定しようとするコードがあると、ビルド時に表示される[エラー一覧]ウィンドウに警告として表示されます。
この機能の何が嬉しいかというと、想定外の所で変数がnullになって不具合になってしまうのを防いだり、nullの可能性を考慮して毎回null判定するコードを書く苦労を軽減したります。
例えば、以下のようなnull判定のコードは記載不要になります。public void Method(object obj) { if (obj == null) { // nullの場合になんらか処理する } .... }最近はこういうnull安全(英語ではnull safety)と言われる対応が各言語で進んでいます。null安全は、安全性を高めると共に、生産性も高めるため、技術者の関心も高く、以下の記事には[いいね]が1600以上ついています。
null安全でない言語は、もはやレガシー言語だということで、この機能はぜひ使いましょう。
null許容参照型の利用方法
まず前提としてC# 8.0を利用するには、Visual Studio 2019が必要で、対象プロジェクトのファイル(.csproj)のLangVersion(C#の言語バージョン)を 8.0 にする必要があります。.csprojのファイルをテキストエディタで開いて編集します。詳細手順は下記を参照ください。
言語バージョンの指定 | ++C++;上記を設定した上で、null許容参照型をプロジェクト全体で有効にするには、NullableContextOptions要素をenableにします。
プロジェクトのファイル(.csproj)の設定例 <PropertyGroup> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </Project>ソースコードの行単位で有効にする場合は、#nullable enable と記載すれば、それ以下のコードで有効になります。#nullable disable と記載すれば、それ以下のコードで無効になります。
#nullable enable // ここ以下の行はnull許容参照型が有効になる object? objA = null; #nullable disable // ここ以下の行はnull許容参照型が無効になる(従来通り) object objB = null;上記はVisual Studio 2019のバージョン16.1時点の情報です。プロジェクト全体で有効にする要素名は、16.2で再び変わるようです(要素名はNullableReferenceTypes→NullableContextOptions→nullableと変わってます)。最新の要素名は下記を参照ください。
null 許容参照型 | Microsoft Docs上記のバージョンでは、プロジェクト全体で有効にすると、.NET Core と.NET Standardのプロジェクトでは正常に動作しますが、.NET Frameworkのプロジェクトでは適切に警告が表示されませんでした。Visual Studio 2019が正式リリースする前(2019年2月)の記事で、以下のような記載を見つけましたが、まだ.NET Frameworkのプロジェクトは対応途中なのかもしれません。
プロジェクトをC# 8とnull許容参照型に対応させる | InfoQ現在,null許容参照型が使用できるのは、.NET Standardプロジェクトと.NET Coreプロジェクトのみですが,Visual Studio 2019が製品化されるまでには.NET Frameworkでもサポートされるものと思われます。
ただ、.NET Frameworkのプロジェクトでも、各ファイルの先頭に #nullable enable と記載すれば、正常に警告表示されました。少し面倒ですが、現行バージョンでもその方法で利用可能です。
ファイルの先頭行に文字列を挿入する方法の検索結果既存プログラムでnull許容参照型を有効にする
既存のプログラムで、この機能を有効にすると、警告だらけになります(手近なプログラム3万行で試した所、400件くらい警告がでました)。
警告を解消するのは面倒ですが、今後も保守し続けていくプログラムであれば、機能を有効にしてソースコードを修正した方がよいと思います(その方が品質も生産性も向上するため)。
既存プログラムを修正する場合に、どうやって直せばよいかについては、以下の記事がとても参考になりました。
プロジェクトをC# 8とnull許容参照型に対応させる | InfoQ
C# 8.0 null許容参照型 | ++C++;既存プログラムを安全にnull安全にする
そして本題です。
既存プログラムをそんな容易に変更できないプロジェクトも多いと思います。
例えば、ソースコードが数十万行あって、品質のリスク(デグレードのリスク)を負えないため、ソースコードは極力変更したくない場合もあると思います(実際、私のプロジェクトはそんな感じです)。折衷案として、新規クラスだけ、#nullable enable を先頭に付けて開発するという方法もありますが、同じプロジェクト内のソースコードの中で、機能が有効なクラスと無効なクラスが混在していると、null許容なのか非許容なのかが分かりづらいため、お勧めしません。やるなら、プロジェクト全体(できればソリューション内の全プロジェクト)で統一した方が効果は高いと思います。
そこで、既存プログラム全体を安全にnull安全にする方法が必要になります。
私が薦める方法は、null許容参照型をプログラム全体で有効にした上で、警告表示された箇所の変数にひたすら 「?」 を付けていくだけの方法です。それならば、品質のリスク(デグレードのリスク)は無いからです。
T と T? の違いは警告が表示されるかどうかだけで、プログラムの挙動は変わりません(T 型の変数に null を設定しても例外は発生しません)。実際の挙動が変わらない変更であれば、それでデグレードが発生するリスクはないという論理です。
例えば、警告の内容が「Null非許容フィールドが初期化されていません」という内容の場合、普通はnullでない値に初期化するように修正しますが、それだと処理ロジックが変わって万が一にもデグレードがあるかもしれないため、変数の型に「?」を付けるだけにします。以下に例を示します。修正前 // このフィールドは初期値がnullになるため // 「Null非許容フィールドが初期化されていません」と警告されます private string message; 普通の修正方法 // 普通は以下のようにnull以外の値で初期化し、null非許容にします private string message = string.Empty; 安全な修正方法 // フィールドの型に「?」を付けて、nullを許容したままにします private string? message;また、単純に「?」を付けるだけでは警告が解消できない箇所は、#nullable disable を記述して、その部分だけnull許容参照型を無効にします。
例えば、ジェネリック型のところに、値型か参照型かどちらも入る可能性がある場合は、?を付けても警告は解消できないため、以下のように部分的に無効にします。これもプログラムの挙動は変わらないため、デグレードのリスクはありません。public class Sample<T> { #nullable disable // T? にしても警告が解消できないためnull許容参照型を無効にする public T GetValue() { ... } #nullable enable }上記の方法であれば、変更の前後でプログラムの挙動は変わらないため、安全にnull安全の状態にできます。
上記の方法は「本来null非許容にできる変数がnull許容のままになってしまう」というデメリットがありますが、それでも警告されなかった変数はnull非許容になりますし、その後の新機能開発はnull安全で開発できる状態になるため、やる価値は高いと思います。
また、いくらかやってみて思いましたが、ひたすら「?」を付けるだけの方法は、考えなくても修正できるので速いです。千件程度の警告でも、ひたすら「?」を付けるだけなら1日でできるんじゃないかと思えました(本稿執筆時点では、まだその規模では実施していないため予想です)。
まとめ
C# 8.0の「null許容参照型」で以下の方法により、既存プログラムを安全にnull安全にする方法を紹介しました。
- 警告表示された箇所の変数にひたすら 「?」 を付ける。
- 上記で解消できない箇所は、#nullable disable で、その部分だけnull許容参照型を無効にする。