はじめに
一番最初に学んだプログラミング言語がJavaだった僕が、C#のプロパティという概念や書き方をあまりよく分かっていなかったので、整理してみたいと思います。
この記事の対象者
- オブジェクト指向の最初の方でよくある説明で、カプセル化の話は分かった人。
- Javaを一通り学んだことがある人で、これからC#を学んでみようと思う人。
- 今一度C#でのプロパティの書き方をさらっと押さえておきたい人。
書き方の比較
前提
オブジェクト指向の説明なんかでよくあるHumanクラスを考えます。Humanクラスは次のようなフィールドと可視性を持っているとします。
- nickname……ニックネームは簡単に変えることができます。制限はなく、自由に取り出したり、書き換えたりすることができます。
- realname……本名はそうそう簡単に変えることができません。ですので取り出すことはできますが、書き換えることはできないということにします。
- age……年齢はサバを読むことができるので、今回は書き換えることができるということにします。しかし、年齢なので0歳以上でなければなりません。
この要件を実現するには次のようなクラスにする必要があります。
- nickname: getter / setterを両方持っている。
- realname: getterのみを持っている。
- age: getterと条件付きsetterを持っている。
さらに今回はインスタンス化時にそのフィールドも同時に設定できるコンストラクタも定義することにします。
Java
Javaの場合は書く量は多いですが、やることはシンプルです。愚直に持つべきフィールドはprivateの変数として定義し、getter / setterはそのままただのpublicメソッドとして定義します。制限をつけたい場合は、メソッドの中でそのロジックを書きます。そもそもアクセスされたくない場合は、メソッドを書かなければいいだけのことです。また、コンストラクタは順番に引数を受け取り、自分自身のフィールドに設定します。
public class Main {
public static void main(String[] args) throws Exception {
Human konan = new Human("工藤新一", "せやかて工藤", 17);
// ニックネーム、本名、年齢は自由に取り出せます
System.out.println(konan.getNickname()); // -> せやかて工藤
System.out.println(konan.getRealname()); // -> 工藤新一
System.out.println(konan.getAge()); // -> 17
// ニックネームと年齢は書き換えることができます
konan.setNickname("江戸川コナン");
konan.setAge(7);
System.out.println(konan.getNickname()); // -> 江戸川コナン
System.out.println(konan.getAge()); // -> 7
// ただし変な年齢を入れても無視されます
konan.setAge(-10000);
System.out.println(konan.getAge()); // -> 7のまま
// 本名を変えることはできません。
// konan.setRealname("江戸川コナン"); // -> そんなメソッドないよって怒られる。
// じゃあコナン君、本名は?
System.out.println(konan.getRealname()); // ->工藤新一。あっ……やっぱり。
}
}
class Human{
private String realname;
private String nickname;
private int age;
public Human(String realname, String nickname, int age){
this.realname = realname;
this.nickname = nickname;
if (age >= 0){
this.age = age;
}
}
public String getRealname(){
return realname;
}
public String getNickname(){
return nickname;
}
public void setNickname(String nickname){
this.nickname = nickname;
}
public int getAge(){
return age;
}
public void setAge(int age){
if (age >= 0){
this.age = age;
}
}
}
Humanクラス内でのコード量が多いですね。今回は条件や制限をつけているのでそこまで嫌な感じはないですが、実質全てのフィールドがアクセスし放題のクラスだったりすると、不毛感が半端ないです。Lombokというライブラリを使うともう少しすっきり書けたりする場合もありますし、IDEの機能を使えばgetter / setterメソッドを自動生成してくれる場合もありますが、それは他の方へ譲ります。
そこで、最近の言語では自分自身のフィールド(アトリビュートなんて言ったりもしますが)getter / setterに関しては特別な書き方を実装していることが多いようです。C#もそのうちの一つです。
C#
C#では、外部からフィールドを書き換えたり取り出したりするにはプロパティという機能を使います。あたかもフィールドを直接書き換えているかのように見えます。しかし、実際はgetter / setterメソッドのようなものを介しているので、これはPascalケースで書きます(C#ではメソッド名はPascalケース)。プロパティを使うと、いちいちメソッドを介している感がなく、直接代入したり、直接参照したりしているように感じますね。
using System;
public class Program
{
public static void Main()
{
Human konan = new Human("工藤新一","せやかて工藤",17);
// ニックネーム、本名、年齢は自由に取り出せます
Console.WriteLine(konan.Nickname); // -> せやかて工藤
Console.WriteLine(konan.Realname); // -> 工藤新一
Console.WriteLine(konan.Age); // -> 17
// ニックネームと年齢は書き換えることができます。
konan.Nickname = "江戸川コナン";
konan.Age = 7;
Console.WriteLine(konan.Nickname); // ->江戸川コナン
Console.WriteLine(konan.Age); // ->7
// ただし変な年齢を入れても無視されます
konan.Age = -10000;
Console.WriteLine(konan.Age); // ->7のまま
// 本名を変えることはできません
// konan.Realname = "江戸川コナン"; // そのプロパティはRead-Only専用と怒られる
// じゃあコナン君、本名は?
Console.WriteLine(konan.Realname); // ->工藤新一。あっ……やっぱり。
}
}
class Human
{
public string Realname{ get; }
public string Nickname{ get; set; }
private int age;
public int Age
{
get
{
return this.age;
}
set{
if (value >= 0)
{
this.age = value;
}
}
}
public Human(string realname, string nickname, int age)
{
this.Realname = realname;
this.Nickname = nickname;
if (age >= 0)
{
this.Age = age;
}
}
}
readもwriteもできるプロパティを使いたいときは、プロパティ名の後ろに{ get; set; }
と書けばいいです。read-onlyのプロパティを使いたいときは{ get; }
にしてください1。setしたりgetしたりするときに内部で何らかの処理を行ってからreturnしたりsetしたりしたいときは、別途内部でフィールド(こっちは普通の変数なのでcamelケース)を用意しプロパティの宣言の後ろに次のように書きます。
{
get
{
// なんか処理
return 処理した結果;
}
set
{
// valueを使ってなんか処理
this.フィールド = 処理した結果;
}
}
このときsetterの内部では、受け取った値は自動的にvalue
という名前の引数で受け取ったことになります。
なお、今回は一部のプロパティがread-onlyであるために、コンストラクタはJavaと同じような感じになりましたが、全てのプロパティにwrite権限がある場合は、コンストラクタをわざわざ用意せずとも、次のような感じに書くことができます。非常にすっきりとしていますね。
Human konan = new Human(){ Realname = "工藤新一", Nickname = "せやかて工藤", Age = 17 };
おわりに
getter / setterの是非はいろいろ言われていますが、それでもオブジェクト指向のカプセル化の話をするうえでは避けては通れないところでもあります。僕はJavaから入ったため、オブジェクトは変数とメソッドだけを持っているというところから理解しました。分かりやすい反面、冗長で面倒だなあという感想を持っていました。
はじめてC#のコードを見たとき、プロパティという機能すら知らなくて、なんでこいつは変数をPascalケースで書いているんだ? と思っていましたが、調べてみるとこんなに簡潔に書けるのかと感激しました。しかし、その一方でgetter/setterにロジックが入ったりすると、微妙に間延びして見にくい感じになったり、またC#はバージョンアップのたびにこのあたりの書き方が進化しているので、調べているうちに「こんなふうにも書けます」とか「かつてはこんなふうに書いていました」みたいな情報がたくさん出てきてかえって混乱したりと、むしろややこしいなという印象も抱いてしまいました。
この記事はそんな自分に対するまとめでもあります。この記事があなたのC#ライフの向上に役立っていただければ幸いです。
追記(2019/3/29)
デフォルト値
上の記事で書くのを忘れていたのですが、C#のプロパティには初期値を与えることができます。例えば、生まれたばかりのHumanオブジェクトに「名無し」というnameを与えたければ、わざわざフィールドを用意して初期化したりコンストラクタを定義したりせずとも、次のように書くことができます。
string name{ get; set; } = "名無し";
これだけでも嬉しいのですが、例えばクラスの中に何らかのクラスのオブジェクトをプロパティとして持ちたいときなどに威力を発揮します。
class Human{
public List<string> Tools{ get; set; } = new List<string>();
}
このようにしてやると、このクラスのオブジェクトの外側からいきなり、このプロパティを操作してやることができます。
var konan = new Human();
konan.Tools.Add("蝶ネクタイ型変声機");
konan.Tools.Add("キック力増強シューズ");
konan.Tools.Add("腕時計型麻酔銃");
プロパティの初期化をしないままこれをやろうとすると、NullReferenceExceptionで落ちてしまいますからね。それを回避するためにはクラスの外でnewしてからつっこまなければなりません。
(thx: 同じ会社のC#erの方)
write-onlyなプロパティ
最初の記事ではwrite-onlyなプロパティは使えない、と書いたのですが、実際はprivate get
なプロパティにしてあげると使うことができるそうです。内部ですら自分のプロパティを読み取れなくなってしまいますからね。
よって、
public string Secret { set; }
と書くことはできませんが、
public string Secret { private get; set; }
と書くことはできます。
var konan = new Human();
konan.Secret = "実は新一";
// Console.WriteLine(konan.Secret); ←Write-Onlyと怒られる
(thx: @munielさん)
uint型
今回はsetterのところでなんらかの処理を行うという前提で書いたのでint型を用いましたが、uint型(Unsigned Int)という符号なし整数という型があるようです。int型では、-2147483648~2147483647を扱いますが、uint型では0~4294967295を扱います。
しかしながら、uint型は基本的に負の数を扱えないため、例えば10 - 20
が-10
とならず、4294967286
(ビット演算のため、ぐるっと回って大きいほうに突入する)となってしまうなど、思わぬ挙動によるバグの温床になりかねません。基本的に算術の対象になりうる値にuint型を使うべきではないようです。
もっとも本当にHumanオブジェクトを作りたければ、ageをset / get可能なプロパティにすると1年ごとに年齢を更新するはめになりますので、たぶん内部に誕生日をプロパティとして持っておき、年齢はそれを元に計算して値を返す get 専用プロパティにするのが良さそうです。
(thx: @Tonbo0710さん、@tak458さん、@Zuishinさん)
コンストラクタでもsetterを使えばいい
public Human(string realname, string nickname, int age)
{
this.Realname = realname;
this.Nickname = nickname;
if (age >= 0)
{
this.Age = age;
}
}
上の記事では、コンストラクタでも変な値が突っ込まれるのを防ぐためにif文を使っていますが、よくよく考えたらここで自分自身のプロパティを経由して値をsetしており、そのsetterの中で判定してくれているので、これは以下のように書いていいはずです2。さらにいうと、プロパティと受け取った変数名がPascalケースとcamelケースで衝突していないので、this
はあってもなくてもよいです。
public Human(string realname, string nickname, int age)
{
this.Realname = realname;
this.Nickname = nickname;
this.Age = age;
}
(thx: ヤマダ)