より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

TimeSpanのToStringで書式指定したら例外! エスケープ必須です

職場でTimeSpan型の値をToStringしてハマった話をします。

DateTime型をフォーマット指定して出力する場合は、以下のような形。

var text = dateTime.ToString("yyyy/MM/dd HH:mm:ss");

f:id:rimever:20190306205528p:plain
.NET FiddleだとDateTime.Now()がサーバー時刻になりますね。ま、そりゃそうか。

よく、Yは大文字なのかとか悩んでGoogleに相談してますが。(実は時間はHでもhでも可能です)

ですが、TimeSpanの場合は、

var text = timeSpan.ToString("hh:mm:ss");

とすると

f:id:rimever:20190306210048p:plain
System.FormatException: Input string was not in a correct format.

になります。

カスタム時間間隔書式指定文字列 | Microsoft Docs

上記のMSDNを読めばわかりますが、hhなどの書式以外の文字列を指定する場合はエスケープが必須となります。たとえスペースでもです。

var text = timeSpan.ToString("hh\:mm\:ss");

としなければなりません。

var text = timeSpan.ToString(@"d\日h\時\間m\分s\秒");

一文字ずつエスケープです。なぜこうなったのでしょうか。

といっても書式以外の文字列に対して''で囲んでもOKのようです。

var text = timeSpan.ToString(@"d'日'h'時間'm'分's'秒'");

'で囲む分、エスケープの1文字に対して2文字使ってしまうので、長い文字列には'、一文字ならエスケープという使い分けを求めているようです。

いや、求めないで欲しいです。

これを読んでいるアナタは笑っていいのですが、私もMSDNを見ました。サンプルソースだけを見て

なんか、余計な文字(エスケープ文字)が入ってるけど、俺のソースにはいらねーや

エスケープせずにコードを動かして何度も例外になってたんですから。

なぜだ!と共有したく検索したら、StackOverflowで同様の質問をしている方がいました。

stackoverflow.com

私の英語力で解釈したところ、「DateTimeのフォーマットは国ごとに違うけど、TimeSpanのフォーマットは国ごとには違わない。DateTimeとTimeSpanのフォーマットが違うのは合理的」といったような回答です。

が、どうなのかな。

だからといって片方はエスケープ、もう片方はエスケープしないフォーマットにする理由があったのかな、と。

とはいえ、別々に設計したからこうなった、というのはそうなんでしょうね。

で、後になって気づいたら、「あ、違っちゃったー」みたいなことなんでしょうかね。

うちの職場なんて同じようなものが全く違った設計で作られていることが、あるあるですし。

WinFormsのTreeViewでNodeの同じレベルで上下入れ替え

WinFormsのTreeViewのNodeを同じレベルでの入れ替えの実装方法です。

f:id:rimever:20190306080105p:plainf:id:rimever:20190306080137p:plain

今時、こんなネタやるのかと自分でも思うのですが、いざやるとなるとGoogleで検索した後、記事が見つからず。

もういいや。もう寝る。寝たら、なんか思いつくだろう。

とかヌルいことを考えたのでメモ。

  1. 選択中のTreeNodeのインデックスを取得
  2. 選択中のTreeNodeを外す
  3. TreeNodeをInsert
  4. 選択中のTreeNodeを指定し直す(そうすると連続で上下に移動させやすい)

以下のような感じ。

        private void MapMoveUpToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var treeNode = treeView.SelectedNode;
            var parentNode = treeNode.Parent;
            var index = parentNode.Nodes.IndexOf(treeNode);
            treeNode.Remove();
            parentNode.Nodes.Insert(index - 1, treeNode);
            treeView.SelectedNode = treeNode;
        }
        private void MapMoveDownToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var treeNode = treeView.SelectedNode;
            var parentNode = treeNode.Parent;
            var index = parentNode.Nodes.IndexOf(treeNode);
            treeNode.Remove();
            parentNode.Nodes.Insert(index + 1, treeNode);
            treeView.SelectedNode = treeNode;
        }

CCleanerはどれくらい容量を削減してくれるのか

私の仮想OSであるWindowsが残容量3.81GB/128GBという状況になってました。

f:id:rimever:20190303095850p:plain

容量を削減せねば、ということで以下のサイトにたどり着きました。

freesoft.tvbok.com

CCleanerは、実際に、どれくらい容量を削減してくれるのかメモとして記録してみます。

インストール時に、ちゃっかりAvast Free Antivirusを一緒にインストールさせようとします。ですが、遠慮しておきます。それで容量を圧迫するじゃないですか。

f:id:rimever:20190303095924p:plain

割とすぐ処理が終わりました。

f:id:rimever:20190303100418p:plain

3.81GBが4.49GBとなりました。あれ、今回は、そんなに改善されなかった。

f:id:rimever:20190303100634p:plain

dotTraceのProfiler Option

Resharper UltimateにはdotTraceも同梱しているので、その使い方についても学んでみることにしました。

Profiler Option

f:id:rimever:20190302174327p:plain

まず、開始時に4つのオプションから選ぶことになります。

  • Sampling
  • Tracing
  • Line-by-line
  • Timeline

詳細は公式に書いてあります。

Profiler Options - Help | dotTrace

職場では、Timelineを使えと言われてました。

せっかくのブログなので、今回は自分の目で確かめてみたいなと思います。

Sampling

f:id:rimever:20190302175129p:plain

Tracing

f:id:rimever:20190302175316p:plain

Line-by-line

f:id:rimever:20190302180434p:plain

Timeline

f:id:rimever:20190302180747p:plain

まとめ

とりあえずTimelineが高機能で一番良さそうです。

ただし、Timelineはアプリの.NetFrameworkのバージョンが低いと出来なかった記憶があります。

公式サイトでは、 SamplingもしくはTimelineが推奨されています。

Samplingでは不十分だった場合に、TracingやLine-by-lineの使用を検討するようです。

Advance

Collect native allocations

f:id:rimever:20190302181956p:plain

以下のダウンロードが必要です。

Download and install the Windows ADK | Microsoft Docs

これを指定した時には、さらに挙動が遅くなりました。

dotMemoryもそうですが、ハイスペックなPCが必要です。

dotMemoryでメモリの状況を把握する

dotMemoryというツールを私は自腹で持っているのですが、ほとんど使ったことがありませんでした。

www.jetbrains.com

職場でメモリリークと格闘した復習がてらブログに書きます。

基本操作

基本的な操作は公式ドキュメントを参考にすればいいと思います。

www.jetbrains.com

ライフゲームのアプリのメモリリークを解消するという実践的なチュートリアルもあります。

実際に動かしてみて思ったこと

チュートリアルのように話は現実は簡単な話ではありません。

GetSnapshotしたデータをCompareして、意図せずメモリが残っていることを見る事も必要ですが、単体で見た方がいいです。

Dominator

その中で、Dominatorタブに切り替えるのがオススメです。

f:id:rimever:20190302001314p:plain

メモリの中で、どのオブジェクトが多くを占めるかを知ることができます。

Inspections

Inspectionsではサマリーを参照することができます。

Event LeakやWPFでのネックとなるBinding Leakを検出するのに適しています。

f:id:rimever:20190302002409p:plain

Similer Retention

このオブジェクトは、どのクラスに属するオブジェクトか?ということを把握するのに役立ちます。

Stringオブジェクトなどは様々なクラスで使われるので、ただStringクラスがメモリリークしていると言われても具体的にどのクラスのメンバかはわかりません。

Dominatorでもその辺りはわかるのですが、Smiler Retentionでは具体的にこのクラスのどのメンバということもわかったりします。

f:id:rimever:20190302003242p:plain

こんなことには巻き込まれないことが理想

メモリ管理をガーベジコレクションに任せて開発効率を高めているので、メモリなんて考えないことが望ましいです。

  • Subscribeしたイベントは、必ずUnsubscribeしなければならない
  • Disposableなオブジェクトは、Disposeすること

など、お作法を守ることが大事ですが、シンプルに開発するのが望ましいです。

アプリとdotMemoryを動かしながら、コードを修正していくことは、メモリの挙動を見るのにとても勉強になります。

機会があれば一度はやってみることをお勧めします。

いきなりメモリリークをなんとかしろと言われると気が重いので、そうなる前に自分のお気に入りのアプリとdotMemoryで動かしてみるのがお勧めです。

複雑になったアプリケーションのメモリリークを完全に取り除くのは困難になります。

使われることがないメモリ割り当ての大きなメンバに対してnullを代入してガーベジコレクションに乗せる事でメモリリークの被害を抑えるという事もしましたが、ソースが意味不明になっていきますし、よろしいとは言えません。

手遅れにならないように、開発していく過程の中でメモリについても気を使って、計測していく方が良いです。

コンソールアプリケーションで0%,1%,2%...と進捗状況を%表示するには

自社の QAエンジニアと共にインストール検証をしていた時のこと。

f:id:rimever:20190228201059p:plain

QAエンジニア「これ、すごくないですか。どうやって表示するんですかね?」

私「確かに」

彼が言っているのは%の部分です、進捗に応じて、コンソール上の%の値が書き換わっています。

「======50%   」

といったように。

C#のコンソール出力だとConsole.WriteLineやConsole.Writeなどで出力はできますし、PythonでもCでもprintメソッドがありますし、bashだってechoで出力できます。

しかし、一度出力した内容を書き直せるのか?

ということは考えたこともありませんし、やったこともありませんでした。

QAエンジニアであれば驚いて楽しんでもらえればいいですが、開発エンジニアの私としては驚いているだけでは立場が無いので試してみることにしました。

ConsoleのSetCursorPositionを使う方法

下記のStackOverflowを読んで知ったのですが、ConsoleクラスにはSetCursorPositionがあります。

stackoverflow.com

using System;
using System.Linq;
using System.Threading;

namespace ProgressConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("進捗を表示します。");
            var top = Console.CursorTop;
            var left = Console.CursorLeft;
            foreach (var value in Enumerable.Range(1,100))
            {
                Console.SetCursorPosition(left, top);
                Console.Write(value + "%");
                Thread.Sleep(100);
            }
            Console.WriteLine("完了");
        }
    }
}

こんな感じ。

f:id:rimever:20190228202731p:plain

ShellProgressBar

より高性能なのを求めるのであれば、ShellProgressBarというライブラリを使うと良さそうです。

f:id:rimever:20190228204120p:plain

考えた見せ方ですね。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ShellProgressBar;

namespace TryShellProgressBar
{
    class Program
    {
        static void Main(string[] args)
        {
            const int totalTicks = 100;
            var options = new ProgressBarOptions
            {
                ProgressCharacter = '─',
                ProgressBarOnBottom = true
            };
            using (var progressBar = new ProgressBar(totalTicks, "Initial message", options))
            {
                foreach (var percent in Enumerable.Range(1,totalTicks))
                {
                    Thread.Sleep(100);
                    progressBar.Tick($"{percent}%まで処理しています。");
                }
            }

            Console.ReadKey();
        }
    }
}

C#で開発するコンソールの文字色の変更。Macではどうなる?

コンソールの文字色を変更する

Console.ForegroundColorを用います。

ポイントとしては、「 Console.ResetColor();」で最後に色を戻してあげることです。

一度、青にしてしまうと、そのまま青のままです。

using System;

namespace ConsoleDotNetCore
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("緑です。");
            Console.ForegroundColor = ConsoleColor.Blue;
            Console.WriteLine("青です。");
            Console.ResetColor();
        }
    }
}

これだけなら、Googleで他の記事を見ればわかる話なので、Macではどうなるんだろうと試してみました。

.Net Coreならば、Macでも動くコンソールアプリケーションが開発できるわけなのですが。

f:id:rimever:20190228140759p:plain

あれ? 青のはずが紫になってしまいました。

ConsoleColor[] Colors // すべての色を列挙
      = (ConsoleColor[])Enum.GetValues(typeof(ConsoleColor));

    foreach (ConsoleColor bg in Colors) {
        Console.ForegroundColor = fg;
        Console.Write(fg);
    }
    Console.ResetColor();
    Console.WriteLine();

f:id:rimever:20190228141317p:plain
Macの場合

f:id:rimever:20190228142026p:plain
Windowsの場合