この投稿はアイスタイル Advent Calendar 2017の12日目の記事です。
現在では、PHP,JSとかを書いてることがほとんどなのですが、
前職では、C#のプロジェクトが多かったので、久しぶりに懐かしさを感じながら思い出しつつ書いてみようと思います。
C#ってなに?
マイクロソフト社製のコンパイル言語で、影響受けた言語がC++,Java,Delphiと言われています。
.NET FrameworkというCLI(共通言語基盤)で動作するプログラミング言語です。「C#.NET」と呼ばれることがあります。
また「VB.NET」と呼ばれるVisualBasicも、.NETFramework上でコンパイル・実行することができます。
実行環境としての.NET Frameworkは基本的にWindowsOS上で実行されています。
最近買ったWindowsPCであれば、.NET Framework 4.5以上はデフォルトでインストールされているかと思います。
C#の歴史
元々C#の生まれ故郷はC言語コンパイラのBCCでお馴染みの「ボーランド社」という企業で「アンダース・ヘルスバーグ」という方が開発していました。
その後、ボーランド社の経営不振になり、派閥抗争の末、アンダース・ヘルスバーグが率いる開発チームが大量リストラにあってしまいました。
そこに目を付けたマイクロソフト社が、開発チームのリストラ対象者を全員受け入れ、現在ではマイクロソフト社としてC#、.NET Frameworkの開発に携わっているそうです。(ビル・ゲイツは賢いですね
ちなみに、TypeScriptの言語設計も関わっているそうな。
なんで「C」の後に「#」が付いてる理由は、
「C++」に更に「++」(更に良く)を付けたかったそうで、
++
++
が、
#
に見えませんか?(俗説です笑
基本言語仕様
よく開発で使用されるIDEとしては「Visual Studio」と呼ばれるIDEで、これもマイクロソフト社製になります。
Javaでいう「eclipse」「IntelliJ」にあたるような重量級IDEポジションかと思います。
Community版であれば、基本的に商用以外の場合に限りライセンス料を支払わずに使用することができます。
基本構文としては、Javaライクに書ける仕様になっています。
生まれた当時はJavaの後追いとしての言語設計になっていたので、Javaを書いたことがある人には、特に大きく躓くこともなくコーディングできると思います。
特に、目立って違いがあるのは仕様を一部紹介してみようと思います。
■ var
var
は、コンパイル時に右辺に対応する型に変換される「暗黙的型」です。
// コンパイル時にintとしてコンパイルされる
var num = 1;
// stringとしてコンパイルされる
var str = "Hello,World";
もちろんこれはできません。
var num = 1;
// numはintとしてコンパイルされるのでコンパイルエラー
num = "1";
var
と聞くとJSをイメージされると思いますが、C#は静的な型付言語なので型が異なる値は格納することはできません。
■ Property, Indexer
「Property(プロパティ)」とは「実装の隠蔽」のことで、Javaでいうgetter,setterが一番近いイメージになるかとおもいます。
プロパティには、それぞれ「getアクセサー」「setアクセサー」があり、それぞれ実装することによって、使用する側は直観的にオブジェクトにアクセスすることができます。
さらに「Indexer(インデクサー)」はプロパティと似ている部分もありますが、インデクサーを実装することによって、
使用する側が[]
を使うだけで、あたかも配列かのようにオブジェクトにアクセスすることができます。
public class UserCollection
{
private IList<User> _users;
public UserCollection(IList<User> users)
{
this._users = users;
}
//// プロパティの実装
public int Count
{
//// get アクセサー
get
{
return this._users.Count();
}
}
//// インデクサーの実装
public User this[int i]
{
//// get アクセサー
get
{
return this._users[i];
}
//// set アクセサー
set
{
this._users[i] = value;
}
}
}
// 使用する側
var collect = new UserCollection(users);
// Countプロパティにアクセス
var count = collect.Count;
// Indexerにインデックスを指定することによってアクセス
var user = collect[1];
collect[2] = new User();
// ※CountプロパティにはSetアクセサーが実装されていないため、コンパイルエラーになる
collect.count = 10;
他にも自動実装プロパティというのがあり、JavaでいうLombokのような機能が標準で実装されています。
public class User
{
//// 自動実装プロパティ。Getアクセス,Setアクセス どちらも可能。
public string Name { get; set; }
}
■ using statement
using (var con = new SqlConnection())
{
// ここで例外がスローされても、conは確実にDisposeが呼ばれる
}
従来の場合、Stream系やConnection系などのクラスをインスタンスすると、アンマネージドリソースなので、GCでは解放されず、明示的にfinalyブロックなどでClose
やDispose
などの解放系メソッドを呼ばなければなりませんでした。
ですが、using
に囲っているオブジェクトはIDisposable
インターフェイスを継承していれば、ステートメント内で例外がスローされても、ステートメント終了後に確実にDispose
メソッドが実行されるようになっています。
追記:Java7から「try-with-resources」っていうのがあったんですね。
■ operator
「operator(オペレーター)」とは、演算子のオーバーロードを可能にする修飾子のことで、演算子を独自にユーザー定義をすることができます。
例えば以下のようなクラスEntityがあったとして、
[Table("user")]
public class User
{
[Key, Column("id")]
public int Id { get; set; }
[Required, Column("name")]
public string Name { get; set; }
}
以下のような、同じ内容のオブジェクトを比較すると
var user1 = new User() { Id = 1, Name = "SCOTT" };
var user2 = new User() { Id = 1, Name = "SCOTT" };
//// false
if (user1 == user2)
{
}
純粋なオブジェクト同士の比較になるので、あたりまえですが比較演算子ではfalse
になります。
ですが、Idが == であればtrueであり、!= であればfalseである
というユーザー定義演算子をoperator
で実装することで、
[Table("user")]
public class User
{
[Key, Column("id")]
public int Id { get; set; }
[Required, Column("name")]
public string Name { get; set; }
public static bool operator ==(User left, User right)
{
return left.Id == right.Id;
}
public static bool operator !=(User left, User right)
{
return left.Id != right.Id;
}
}
var user1 = new User() { Id = 1, Name = "SCOTT" };
var user2 = new User() { Id = 1, Name = "SCOTT" };
//// Idが同じなのでoperatorによってtrueになる
if (user1 == user2)
{
}
operatorの==
と!=
をオーバーロード実装することで、組み込み型と同じような記述で比較をおこなうことが可能になります。
その他「単項演算子」「2項演算子」「比較演算子」等、operator
によって実装することができます。
ただ、演算子の概念を大きく返ることが可能になってしまうので、実装には注意が必要です。
■ その他、、、
他にも「匿名クラス」「関数ポインタのデリゲート」「Nullable<T> Null許容型」「ダックタイピングを実現するdynamic」「非同期処理のTask async/await」「複数の戻り値のタプルリターン」等、
コーディングがしやすいように随所に工夫されているので、興味があったら調べてもいいかと思います。
LINQ(Language INtegrated Query)
決して、アイドルのLinQのことではありません。
(たまに本当にこっちのLinQでググった人がMSDNとかに迷い込むことがあります。
統合言語クエリと呼ばれるもので「Language INtegrated Query」の略で、通称「LINQ(リンク)」と呼ばれています。
実行条件としては、.NET Framework3.5以上であれば使用することができます。
簡単に説明するとコレクション(ListやArray等)の集計関数のようなもので、Java8のStreamAPIに似ているかと思います。
この機能は非常に便利で、C#を採用する大きなメリットの1つになることが多いです。
LINQには「クエリ式」「メソッド式」という記述方法があります。
クエリ式は、SQLライクな記法で集計することができ、メソッド式は集計条件そのものをメソッドチェーンにすることによって集計をすることができます。
以下のコレクションを基に、いくつかの使用例を挙げてみます。
var users = new List<User>() {
new User() { Id = 1, Name = "SCOTT", BirthDay = new DateTime(1987, 2, 19), Sex = 1, IsDeleted = false, },
new User() { Id = 2, Name = "NICK", BirthDay = new DateTime(1949, 5, 20), Sex = 2, IsDeleted = false, },
new User() { Id = 3, Name = "JOHNNY", BirthDay = new DateTime(1958, 3, 4), Sex = 2, IsDeleted = false, },
new User() { Id = 4, Name = "BOB", BirthDay = new DateTime(1997, 11, 17), Sex = 1, IsDeleted = false, },
new User() { Id = 5, Name = "FRED", BirthDay = new DateTime(1982, 3, 20), Sex = 1, IsDeleted = false, },
};
■ Where, Select
//// クエリ式
var query = from u in users
where u.IsDeleted == false
select u.Name;
//// メソッド式
var method = users
.Where(u => u.IsDeleted == false)
.Select(u => u.Name);
「usersのコレクションで、IsDeletedがfalseであるNameを取得」という集計を行っています。
上記のクエリ式、メソッド式の集計結果はほぼ等価です。
クエリ式where
、メソッド式Where()
で、IsDeleted == false
の条件を指定し、
メソッド式はSelectは省略可能で、集計後のUserコレクション(IEnumerable<User>型)を返すことができます。
ただし、クエリ式の場合はselect
は省略することができませんので、Userコレクションを返すのであれば、select u;
というように記述すれば可能です。
■ OrderBy, OrderByDescending
//// クエリ式
var query = from u in users
where u.IsDeleted == false
orderby u.Sex ascending, u.Id descending
select u.Name;
//// メソッド式
var method = users
.Where(u => u.IsDeleted == false)
.OrderBy(u => u.Sex)
.ThenByDescending(u => u.Id)
.Select(u => u.Name);
上記のWhere,Selectの条件に加えて、orderby句で「usersの中で、Sexの昇順にしつつ、Idで降順にする」並び替え条件を追加しています。
メソッド式のみ、昇順の場合はOrderBy(...)
、降順の場合はOrderByDescending(...)
で並び替え条件指定をします。
クエリ式は、orderby
に第2キー以降も続けて条件追加できますが、メソッド式の場合、第2キー以降はThenBy(...)
またはThenByDescending(...)
で指定しなければなりません。
勘のいい人は気づいてるかと思いますが、OrderBy(第1キー).OrderBy(第2キー)
と書いてしまうと、最後のOrderBy(第2キー)
のみで並び替え集計がされてしまうので、注意が必要です。
■ Join, GroupJoin
//// Petのコレクション
var pets = new List<Pet>() {
new Pet { Id = 1, Name = "Barley", UserId = 1 },
new Pet { Id = 2, Name = "Boots", UserId = 1 },
new Pet { Id = 3, Name = "Whiskers", UserId = 2 },
new Pet { Id = 4, Name = "Blue Moon", UserId = 4 },
new Pet { Id = 5, Name = "Daisy", UserId = 4 },
};
//// クエリ式
var query = from u in users
join p in pets on u.Id equals p.UserId
where u.IsDeleted == false
select new {
UserId = u.Id,
UserName = u.Name,
PetId = p.Id,
PetName = p.Name,
};
//// メソッド式
var method = users
.Join(pets,
u => u.Id,
p => p.UserId,
(u, p) => new { u, p })
.Where(j => j.u.IsDeleted == false)
.Select(j => new {
UserId = j.u.Id,
UserName = j.u.Name,
PetId = j.p.Id,
PetName = j.p.Name,
});
上記はPetクラスのpetsコレクションとusersコレクションをInner JoinしたLINQになります。
UserのIdプロパティを基に、PetのUserIdプロパティとINNER JOINの内部結合になります。
//// クエリ式
var query2 = from u in users
join p in pets on u.Id equals p.UserId into p
from p2 in p.DefaultIfEmpty()
where u.IsDeleted == false
select new
{
UserId = u.Id,
UserName = u.Name,
PetId = p2?.Id,
PetName = p2?.Name,
};
//// メソッド式
var method2 = users
.GroupJoin(pets,
u => u.Id,
p => p.UserId,
(u, p) => new {
u = u,
p2 = p.DefaultIfEmpty()
})
.Where(j => j.u.IsDeleted == false)
.SelectMany(j => j.p2,
(j, p2) => new {
UserId = j.u.Id,
UserName = j.u.Name,
PetId = p2?.Id,
PetName = p2?.Name,
});
こちらは、Left Join である外部結合を行っています。
見てわかると通りInner Join時のLINQと大きく異なる部分があるかと思います。
クエリ式の場合、Joinされたpetsであるp
を.DefaultIfEmpty(...)
メソッドでp2
に格納しています。
.DefaultIfEmpty(...)
はコレクション内に存在しない要素の場合、その要素の規定値(今回はnull)を返すようにします。
例えるならば、SQLのLEFTJOIN時に結合条件で存在しなかった左部、とイメージしてもらえればわかりやすいかと思います。
メソッド式の場合も同様に、GroupJoin(...)
内で.DefaultIfEmpty(...)
を行っています。
またSelectMany(...)
でPetコレクションであるp2を平坦化しています。
■ Any, Single, First, Last
//// 存在する場合はtrue
var any = users.Any(u => u.Id == 1);
//// 1件の要素を取得する
var single = users.Single(u => u.Id == 1);
//// 最初の要素を1件取得する
var first = users.First(u => u.Sex == 1);
//// 最後の要素を1件取得する
var last = users.Last(u => u.Sex == 1);
Any(...)
はコレクションに条件が合致する要素が存在する場合はtrue、存在しない場合はfalseが返されます。
Single(...)
はコレクションに条件が合致する1件の要素を取得します。
1件以上存在する場合、InvalidOperationException: 'シーケンスに複数の一致する要素が含まれています'
として例外がthrowされます。
First(...)
はコレクションに条件に合致する要素の中で、最初の要素を取得します。
Last(...)
はコレクションに条件に合致する要素の中で、最後の要素を取得します。
Single,First,Last
ともに条件に合致する要素が存在しない場合、InvalidOperationException: 'シーケンスに、一致する要素は含まれてません'
と、例外がthrowされます。
また、このLINQはメソッドのみの実装で、クエリ式と組み合わせると以下のようになります。
//// IsDeleted == false に合致する最初の要素を取得する
var user = (from u in users
where u.IsDeleted == false
select u)
.First();
■LINQ to SQL, LINQ to Entities
実は上記たちのLINQは「LINQ to Object」という、オブジェクトに対してのLINQによる集計処理でした。
LINQには他にも種類があり、「LINQ to SQL」「LINQ to Entities」など、LINQのコードからSQLを自動生成をするフレームワーク(Entity Framework)も開発されています。(LaravelのEloquentに似ています。
クロスプラットフォームも最近頑張ってるよ
ちょっと昔までは、C#は基本的にWindows環境でしかコンパイル、実行ができませんでした。(一応Monoとかあるけど、自分は現場では見たことありません...
ですが、最近では***「.NET Core」というクロスプラットフォームにも対応されたフレームワークがマイクロソフトからリリースされています。(しかもオープンソース!
まだWindowsOSの.NET Frameworkとは機能差はありますが、このフレームワークの登場により、他のOSでもC#を使用して開発をし、コンパイル・実行が可能となりました。(JavaVMみたいですね!
また、クロスプラットフォームの対応が可能となり、iOS、Android、のアプリケーションもC#.NETを使用して開発ができるXamarin***というフレームワークも開発が進んでます。
(Xamarinのマイクロソフトエバンジェリストちょまどさん可愛いです。
また、自分の勝手な主観なんですが、C#の言語設計思想を見ていると、
「複雑化してしまうコードを、いかにして短く書き、かつ解りやすく、美しく書くのか」
という部分に、注力している気がします。
そんなC#と.NET Frameworkを社会人になってからプログラマーとして使っていたので、クラス設計の考え方や、アーキテクチャ、フレームワークとしての思想・理念などを多く学べることができて、この言語には大変感謝をしています。
また、前職の最後はC#で.NET Frameworkを拡張した、ちょっとした社内フレームワークを開発したりなんかもしてました。
どうでしょうか。そんなこんなで、少しC#に興味を持ってもらえたら幸いです。
明日のアイスタイル Advent Calendarは@grassyさんの「Goでのなんちゃって画像処理」ですー。お楽しみにー