ブログトップ 記事一覧 ログイン 無料ブログ開設

子持ちししゃもといっしょ RSSフィード

2015-03-05

よい本を読んでもなかなか使いどころが難しいという話

先日、会社で欲しい本を買ってくれるというのでこの本を買ってもらいました。

より良いコードを書くために...という副題が付いているとおり、本書はコードを書く上で気を付けるべき点について書かれている本です。

わたしがプログラムを書き始めたのは大学4年生のときなので書き始めて15年くらい経つのですが、いまだに「これだ!」という自分のスタイルを見つけかねています。「変数の名前はどういう感じに付ければいいのか?」「コメントに何を書けばいいのか?」といった基礎的な部分から「処理の分割単位はどのくらいの規模にするのか?」とか具体的な部分に至るまで、長く続けてきたからこそ身に付いている勘どころみたいなものはありますが、それがベストなのか?というとそうでもないなという気がしてならないのです。

読みやすいコード、修正しやすいコード、拡張性のあるコード。

そんなコードの書き方に関するパターン集とも言えるのが本書「リーダブルコード」でして、まだ全部は読んでいないのですがとてもおもしろいです。自分の考えが正しかったんだなと思える部分もあれば、逆に考えが足りなかったんだなと思える部分もあって久しぶりにあちこちに付箋を貼りながら読んでます。

買ってもらってよかった!


そんなわけで、せっかくいい本を読んでるわけですからいいなと思ったところから取り入れていきたいなと思ってみたものの、意外に難しいことに気付いたのです。


例えば「変数名を分かりやすくする心がけ」という部分を読んでから過去に書いたソースを読むと非常に分かりにくい名前を付けていることが分かります。名前を見れば何が格納されているのか分かるようにすべき!と言われるとそうだなと思うのですが、じゃあいま動いているプログラムのソースにさっと手を入れてリファクタリングするのか?というとなかなかそこまではできません。

変数名だけの変更であればそこそこ自動でできるとは言え、直したら直したでデグレードがないかどうか確認しなければなりませんし、まして気になる変数を全部直すとなればかなりのボリュームですので直し終わった後のテストは単体テストだけではやや不安が残ります。

とりあえずできるところから少しずつ直していくというのが正しいというのはわかりますが、こういうのはある程度まとめてやらないと読みやすさにばらつきが出て非常に管理しにくくなります。あと、直すのに時間をかけてしまうとどこまで直してどこから直してないのかがはっきりしないなんてことも出てくるわけでやるならガツンとやらないと中途半端に終わってしまいます。

でもガツンと直すとテストが大変過ぎて手が回らないと...。


こういう「改善は部分的にでもするべきなのかそれとも全体で同じレベルにすべきなのか」という話は、実はプログラムを作っているとわりとよく出てきます。C#のように言語のバージョンアップが頻繁に行われるようなケースは特にそれが顕著で、以前はベストだった方法が陳腐化しやすいという状況がよく発生します。

例えばデータベースへのアクセス方法がわかりやすいのですが、接続型(SqlConnection/SqlCommand)だけだった時代はあっという間に過ぎ去り、非接続型(SqlDataAdapter/Dataset)での方法が主流になったかと思えばLINQやEntity Frameworkが出てきてそちらがいいよねという話になっています*1

このケースの具体的な例を挙げると、最初は接続型で作っていたシステムに機能を追加しようとしたらEntity Frameworkという非常に便利なものが増えていた場合、機能追加する部分は接続型で作るべきなのかそれともEntity Frameworkで作るべきなのかで悩みます。

接続型で作れば昔作った部分と同じような内容になるのでメンテナンスは非常にしやすいのですが、最新の便利機能の恩恵にはあずかれなくなりますし、逆にEntity Frameworkで作ると開発効率はよくなるものの他のDBアクセス部分とは作りが異なるためにメンテナンスはしにくくなります。

どちらがいいとか悪いとかという話ではなく、一部にしか適用できない改善事項を積極的に取り入れるべきかどうかという選択の問題をわたしは判断しかねているという話です。


そんなわけでせっかく良い本を読んだのに、なかなか適用する勇気がもてなくてもったいないなーと悶々としている毎日です。

*1:システムの形態によって向き不向きも多少ありますが

2014-12-19

ずっと勘ちがいしていたstring.Format()の挙動というか仕様

先日、ちょっとしたプログラムを作っていたところ、いままで(少なくとも5年くらい)ずっと勘ちがいしていたことに気付いたことがあったのでメモ代わりに残しておきます。


C#にはstringクラスという文字列を扱うためのプリミティブな型があります*1

このクラスには文字列を操作するための便利なメソッドがたくさん実装されています。その中でもわたしがすごく好きでよく使っているのがFormatメソッドです。これはどういうメソッドかというとCでいうところのprint系の関数と同じでして、文字列を整形して出力するためのメソッドです。


どんな感じで使うのかと言うとこんな感じで使います。


var x = 123;
Console.WriteLine(string.Format("(1) {0}\r\n(2) {0:00000}\r\n(3){0:D5}", x));

// ↓出力例
// 
// (1) 123
// (2) 00123
// (3) 00123

*2


第一引数には文字列を入れるのですが、その中に{n}という部分を入れておくと第二引数以降で渡した値がここに埋め込まれます。

第二引数が0番目になるので{0}、第三引数は1番目なので{1}という感じです。


また、上の例では{0:00000}とか{0:D5}とありますがこれはどちらも同じ意味で「先頭をゼロ詰めにして5桁で表示する」という指示です。


こんな感じですごく簡単なのに便利なので重宝していたのですが、実はstring.Formatの第二引数以降に渡す値が数値ではなく文字列の場合は先頭ゼロ詰めという指示が有効ではないらしいのです。


どういうことかというとこんな感じになります。


var x = 123;
Console.WriteLine(string.Format("(1) {0}\r\n(2) {0:00000}\r\n(3){0:D5}", x.ToString()));

// ↓出力例
// 
// (1) 123
// (2) 123
// (3) 123

最初の例ではintが第二引数に渡っていたのですが、この例ではそれをstringに変換してから渡しています。すると指定した書式はすべて無視されてそのまま表示されます。


もともとD5というのはDecimalのDだろうから数値前提だと言われたら返す言葉がないのですが、でもなあ...。


え?当たり前??

*1:厳密にはstringはSystem.Stringのエイリアスですがここではとりあえず置いておきます

*2:Console.WriteLine()自体がstring.Formatと同じような形式で整形する文字列を受け取れるのですが、ここでは分かりやすくするためにあえてstring.Formatを入れています。

2014-06-26

SQLServerの動的管理ビューについて調べてみた


作りかけのツールで必要になったのでSQLServerの動的管理ビューについて調べてみました。

参考にしたのは以下のサイト。



いま作っているのは「特定のテーブルのデータをオンメモリ(キャッシュ)に配置するためにクエリーを投げるツール」なのですが、その効果を知るためにクエリーを投げる前後のデータキャッシュのサイズを取得する必要があってそれに使える機能がなにかないか調べていて動的管理ビューのことを教えてもらいました。


最終的には「SQL Server オペレーティング システム関連の動的管理ビュー (Transact-SQL)」のsys.dm_os_buffer_descriptorsを使って実現しました。


SELECT (COUNT(*) * 0.008024) AS cached_size ,name
  FROM sys.dm_os_buffer_descriptors AS bd 
    INNER JOIN 
    (
        SELECT object_name(object_id) AS name 
            ,index_id ,allocation_unit_id
        FROM sys.allocation_units AS au
            INNER JOIN sys.partitions AS p 
                ON au.container_id = p.hobt_id 
                    AND (au.type = 1 OR au.type = 3)
        UNION ALL
        SELECT object_name(object_id) AS name   
            ,index_id, allocation_unit_id
        FROM sys.allocation_units AS au
            INNER JOIN sys.partitions AS p 
                ON au.container_id = p.partition_id 
                    AND au.type = 2
    ) AS obj 
        ON bd.allocation_unit_id = obj.allocation_unit_id
 WHERE database_id = DB_ID()
 GROUP BY name, index_id 
 ORDER BY cached_pages_count DESC;

ほとんどサンプルのままなのですが、サンプルだと1列目のcached_sizeはページ数になってしまうので8KBをかけて1024で割ることでMB単位に変換しています。あと不要な列を削除したり。


この動的管理ビューはSQLServer2005からの機能だそうでして、SQLServer2014が出ていることを考えればかなり前からあるのに気付いていなかったことになります。もったいない。。。たいへん便利なのでこれからは積極的に使っていきたいです。

2012-11-26

列挙型のFlags属性について

先日、プログラムを作っていたらC#の列挙型にはFlags属性というおもしろい属性が付けられることを知ったのでちょっと使ってみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EnumTest {
     class Program {

          /// <summary>
          /// 体の部位を指定
          /// </summary>
          [Flags]
          enum 部位 {
               頭          = 1,
               右肩     = 2,
               左肩     = 4,
               右腕     = 8,
               左腕     = 16,
               おなか     = 32,
               腰          = 64,
               おしり     = 128,
               右足     = 256,
               左足     = 512,
          }

          /// <summary>
          /// 現在の行動を指定
          /// </summary>
          enum 行動 {
               停止 = 1,
               歩く = 2,
               走る = 4,
          }

          static void Main(string[] args) {
               ShowBuiEnum();
               ShowKoudouEnum();
               Console.ReadLine(); // ここで止める
          }


          static void ShowBuiEnum() {
               Console.WriteLine("【部位の列挙型テスト(Flags属性付与)】");
               Console.WriteLine("部位.おなか={0}", 部位.おなか);
               Console.WriteLine("部位.頭 | 部位.おなか | 部位.腰={0}", 部位.頭 | 部位.おなか | 部位.腰);
               Console.WriteLine("部位.右肩 | 部位.腰 | 部位.左足 | 部位.右腕={0}", 部位.右肩 | 部位.腰 | 部位.左足 | 部位.右腕);
               Console.WriteLine();
          }

          static void ShowKoudouEnum() {
               Console.WriteLine("【行動の列挙型テスト(Flags属性無し)】");
               Console.WriteLine("行動.走る={0}", 行動.走る);
               Console.WriteLine("行動.停止={0}", 行動.停止);
               Console.WriteLine("行動.歩く | 行動.停止={0}", 行動.歩く | 行動.停止);
               Console.WriteLine("行動.走る | 行動.歩く={0}", 行動.走る | 行動.歩く);
               Console.WriteLine();
          }
     }
}

(実行結果)

f:id:itotto:20121127092715p:image


説明が前後してしまいましたが、Flags属性というのは「列挙型をビット フラグとして定義する」ための属性です。

Flags属性を付ける/付けないというのは、列挙型の各項目が完全独立かどうかで判断できます。

つまり列挙型の項目それぞれがお互いに影響しあうことがない場合、たとえば項目の重ね合わせに矛盾が生じないケースにFlags属性は利用できます。逆に赤信号と青信号のような同時に取りえない状態(排他状態)を表す場合にはFlags属性は付けません。

2012-06-18

ユニークなファイル名を取得する方法


先日、大きなサイズのXMLファイルを解析して変換し、小さなサイズのXMLファイルを出力するというプログラムをC#で作りました。


XMLの解析や作成はLINQを使えばかなり簡単なのでその部分のロジックはまったく問題なくできたのですが、出力するファイル名の付け方を間違ってしまうというミスをしてしまいました。


もう少し詳しく書くと、A.xmlというXMLファイルを10個のファイルに分割したときに、分割後のファイルの名前をA_yyyyMMddhhmissfff.xmlとしたところ、ファイル名が重複してしまったというミスです。


ここでyyyyMMddhhmissfffというのは、日時を入れるという意味でして、たとえば2012年6月18日12時34分56秒789ミリ秒に処理をした場合にはA_20120618123456789.xmlという名前を付けることになります。


なんでこれがダメだったのかはもう一目瞭然ですが、XMLを作るのに要した時間が1ミリ秒以下だった場合に名前が同じXMLができてしまうというミスです。初歩的すぎる...。


これはこれで手抜きをすると痛い目にあうというよい教訓になったわけですが、せっかくなのでファイル名を自動生成する方法についてまとめたいと思います。


1. Path.GetTempFileName()を使う

一時ファイルを作成する方法を調べた時に一番最初に出てきたのがこのPath.GetTempFileName()というメソッドでした。


(メリット)

    • メソッドを呼び出すだけでファイル名が取れて楽

(デメリット)

    • ファイルの出力先フォルダやファイルの拡張子が選択できない
    • IOExceptionを出す場合がある

試しに使ってみたところ、以下の仕様で動作しているようでした。


出力先%TEMP%
ファイル名tmp????.tmp
その他メソッドを呼び出したタイミングでサイズが0Byteのファイルが作られる

ファイル名の????には0000-FFFFまでの文字列が順番に入るようです。これがFFFFまでいってしまうと、次の呼び出しがIOExceptionになりますので、つまりは65535回しかこのメソッドは呼べないことになります。もちろん使い終わったファイルは消せばよいのですが、そもそも「一意なファイル名が欲しい」だけなのに、使い終わったあとの始末までやることを強制されるのってなんか違う気が...。いや、やることはやるんですが、本末転倒な気がしてなりません。


とか書いてたらちゃんとMSDNに書いてました。


一意な名前を持つ 0 バイトの一時ファイルをディスク上に作成し、そのファイルの完全パスを返します。

Microsoft のテクニカル ドキュメントの以前のバージョン | Microsoft Docs

このメソッドは、.TMP という拡張子を持つ一時ファイルを作成します。

以前の一時ファイルを削除せずに、GetTempFileName メソッドを使用して 65535 個を超える数のファイルを作成した場合、IOException が発生します。

GetTempFileName メソッドは、一意な一時ファイル名が使用できない場合に、IOException を発生させます。このエラーを解決するには、すべての不必要な一時ファイルを削除します。

Microsoft のテクニカル ドキュメントの以前のバージョン | Microsoft Docs

このあたりの不便さ(出力先や拡張子が選べない)は、staticなメソッドを呼び出すだけで使えるという気軽さとトレードオフした結果だと思うのでここはしょうがないのかなと。不便とはいえ、たとえば出力した後に名前を変えて移動すれば解決できる程度の問題ですのクリティカルな問題点ではありません。


ただ、そういう不便さは許せる一方で、使うためには決まりごとがいくつか生じてしまうことやエラーハンドリングが必要なこと、そして「ファイル名が欲しいだけなのに後始末まで約束させられる」というのは個人的にはいただけないなーと思います。


いまのところ「めんどくさいから使わね」という結論に...。


2. データのユニークキーを流用する

作成するファイルに格納されるデータのキー情報(個人や商品のIDなど)がある場合には、それをファイル名に入れることでユニークなファイル名が作成できます。


(メリット)

    • データの中とファイル名が結びついているので分かりやすい

(デメリット)

    • ファイルの中にキー情報が無いとそもそも使えない
    • 同一キーに対して複数のファイルが必要になった場合など、これ単体では使えないケースがある

アイディアとしては悪くないのですが、基本的にこれだけではユニークなファイル名は生成できません。

それは他のほとんどのアイディアも同じなんですが...。


3. 連番を利用する


ファイル名に処理をした順番に連番を振るという方法です。

ベタといえばベタなんですが、ハンドリングしやすいですしとても確実な方法です。

さらに処理した順番もわかるので個人的にはすごく好きな方法です。


int cnt = 1;
string before = DateTime.Now.ToString("yyyyMMdd");

foraech (xxxxxxx){ // ここのループ条件は適当に
  string now = DateTime.Now.ToString("yyyyMMdd");
  if (before != now) cnt = 1;
  string.Format("A_{0}_{1:000}.xml", DateTime.Now.ToString("yyyyMMdd"), cnt++);
  before = now;
}

ここでは日付ごとに連番を振っていますが、このあたりは好き好きで。


(メリット)

    • ファイルを作成した順番がわかる

(デメリット)

    • 連番だけではユニークであることを保証できない

上の例だと日付と組み合わせて使っていますが、こんな感じで連番を振るにしても他の方法との併用が望ましいです。



4. 乱数/ハッシュを利用する


ここまで引っ張ってしまいましたが、プログラマーが一意なデータを得ようと最初に思いつくのはおそらく乱数orハッシュだと思います。好みはあるでしょうけど。


乱数だとこんな感じ。

foraech (xxxxxxx){ // ここのループ条件は適当に
  string.Format("A_{0:0000000000}.xml", new Random().Next());
}

ハッシュはいいサンプルが思いつかないので、参考サイトを紹介するにとどめておきます。ファイルがそれほど大きくなければ、ファイルの中身全部でハッシュ値を生成するのがよいのかなと。


(参考) MD5やSHA1などでハッシュ値を計算する



(メリット)

    • 扱いが簡単な割に効果は大きい

(デメリット)

    • そもそも一意であることを完全に保証する仕組みではない
    • 名前からファイルの中身を判断するのが難しい

確率的には被る可能性はほぼゼロなんですが、ゼロではないだけで可能性としてはありえます。

なので仕組みとしてそもそも不完全なのと、あとはファイル名が無作為過ぎてファイルの中と結びつかないのは個人的にはあまり好きではないです。なので私自身はあまり使いたい方法ではありません。


5. PIDを利用する

プロセスごとに振られたIDを使えばユニークになるんじゃね?という安直な方法です。


int cnt = 1;
int pid = Process.GetCurrentProcess().Id;
foraech (xxxxxxx){ // ここのループ条件は適当に
  string.Format("A_{0}_{1:000}.xml", pid , cnt++);
}

(メリット)

    • 呼び出すだけで簡単

(デメリット)

    • 常駐型プロセスだとIDは変わらない

とりあえずサービスとして動かしている場合などはプロセスIDは常に変わらないのでこれだけはキー情報になりません。日付や連番と組み合わせて使うのがよいのかなと。というか、それだったら別にプロセスIDはいらないか...。


GUIDを利用する


GUID(Global Unique IDentifier)、日本語だとグローバル一意識別子というそうですが、WikipediaによるとUUID(Universally Unique IDentifier)のMS版実装を指すそうです。ただ、MS版と言いつつも多くのサービス、ソフトウェアで使われているために概ね一般的な言葉ととらえてもよさそうです。


MSDNを読んで気付いたのですが、これクラスじゃなく構造体なんですね。ただC#の場合は構造体もメソッドをもてるので、値型か参照型かとか配置される場所がヒープかスタックかくらいしか違いはなくて扱いは変わんないので


GUID は、一意な識別子が必要とされるコンピュータおよびネットワーク全体で使用できる 128 ビットの整数 (16 バイト) です。このような識別子は、重複する確率がかなり低くなっています。

Guid 構造体 (System)

これも使い方は簡単で、NewGuidメソッドで初期化してからToString()するだけです。

    Guid guidValue = Guid.NewGuid();
    Console.WriteLine(guidValue.ToString()); // -が付いてる
    Console.WriteLine(guidValue.ToString("N")); // -が付いてない
    Console.WriteLine(guidValue.ToString("B")); // -が付いていて、{}で囲まれている
    Console.WriteLine(guidValue.ToString("P")); // -が付いていて、()で囲まれている

で、結果はこんな感じです。

f:id:itotto:20120613132437p:image


新しいGUIDが欲しい場合は再度NewGuidメソッドを呼べばOKです。


(参考) GUID値を生成するには?


(メリット)

    • 呼び出すだけでほぼ一意な文字列が取れて便利

(デメリット)

    • 任意過ぎて名前に規則性がなく扱いにくいケースがある
    • 確実にファイル名がかぶらないわけではない

かなり便利ですし、名前が被る確率もほぼないので悪くないのですが、これも乱数などと同じで規則性が無さすぎてファイル名としてはとても扱いにくいです。

あとこれもここまで紹介したとおり、これだけで完全に重複を防ぐ方法ではないので、他の方法と同じく単体で使うのではなく要素として使うべきかなと。


6. 結論

結局、今回私が選んだのは「日付(yyyyMMdd)」+「ユニークな情報を付与」+「連番」という組み合わせでした。


public string GetUniqueFileName(string id, ref int num){
    while (true){
        string fileName = string.Format("XMLファイル_{0}_{1}_{2:00000}.xml", DateTime.Now.ToString("yyyyMMdd"), id, num++);
        if (File.Exists(fileName)) {
            if (num > 99999) throw new IOException("連番でか過ぎワロタ");
            continue;
        }
        return fileName;
    }
}

連番を5桁(99999まで)にしているのは、現実的に作成される上限とファイル名が極力短くしたいというところで折り合いをつけて決めただけでここはケースバイケースで変えてよいです。


あとソースにもあるとおり、既にその名前のファイルがあった場合には「連番」をインクリメントして再度名前を変えるという対応も合わせて行ったので、これでファイル被ることはなさそうです。本当は抜け番があった場合にも対応しようかと思ったのですが、現実的にそこまでファイルは増えないのでここでは割愛しました。


ちなみに、今回はシングルスレッド前提の処理なのでこの程度でも十分ですが、マルチスレッドな環境だともうちょっと工夫や気配りが必要かもです。

# 面倒なので今回はそこまでは説明しません(詳しくないし)


もっと良い方法があるよ!という人はコメントで教えてくださいー。