int[] a = new int[]{1, 3, 5};
for(int i=0; i< a.Length; ++i) {
Console.Write("{0}\n", a[i]);
}
// List を使った実装.
List list = new List();
list.Add(7);
list.Add(5);
list.Add(3);
list.Add(1);
for(Node n=list.head; n!=null; n=n.next)
{
Console.Write("{0}\n", n.elem);
}
// Enumerator 経由で Access する
int[] array = new int[]{1, 3, 5, 7};
IEnumerator e = array.GetEnumerator();
while(e.MoveNext())
{
int val = (int)e.Current;
Console.Write("{0}\n", val);
}
// foreach 文を利用する.
foreach(型名 変数 in コレクション)
文
int[] array = new int[10]{1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
foreach(int n in array)
{
Console.Write(n + " ");
}
___
■ Enumerator.を自作する
___
■ XML
___
■ XMLDocument
using System.Xml;
{
//インスタンスの生成
XmlDocument doc = new XmlDocument();
//XMLの読み込み
doc.Load("d:/test.xml");
//ルートノードの参照
XmlNode root = doc.DocumentElement;
//要素ノードの作成して追加
XmlElement e = doc.CreateElement("test");
root.PrependChild( e );
//テキストノードの作成
XmlCharacterData text = doc.CreateTextNode("foo");
e.PrependChild( text );
// Attribute 追加
e.SetAttribute( "id", "10" );
//XMLの保存
doc.Save( "d:/out.xml" );
}
___
■ ファイル(File)
___
■ 書き込み
using System.IO;
{
FileStream f = new FileStream( "d:/test.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite );
StreamWriter s = new StreamWriter( f );
s.WriteLine("aaa");
s.WriteLine("bbb");
s.Flush();
}
___
■ ロック(Lock)
{
FileStream f = new FileStream( "d:/test.txt",
FileMode.Create,
FileAccess.ReadWrite,
FileShare.ReadWrite
);
StreamWriter s = new StreamWriter( f );
s.WriteLine("aaa");
s.WriteLine("bbb");
s.Flush();
// ファイル全体をロックする
f.Lock(0, f.Length);
// 何か長い処理をする。
// ロック中は他のプロセスはアクセスできない。
// プロセスはファイルにアクセスできません。別のプロセスが使用中です。
Thread.Sleep( 30 * 1000 );
f.Unlock(0, f.Length);
}
// ロックをすることで、他のプロセス(コード)からアクセスすると IOException が発生する。
f.Lock(0, f.Length);
FileStream f2 = new FileStream( "d:/test.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite );
StreamWriter s2 = new StreamWriter( f2 );
s2.WriteLine("aaa");
s2.WriteLine("ccc");
// 厳密には Write() で追加したバッファの内容がファイルへ転送した瞬間におきる。
s2.Flush();
s2.Close();
POINT
C# の配列はひとつのクラスのため Instance を生成する時は new をする。
System.Array クラスから継承される。
次元は N 次元可能で, 宣言. 初期化の構文以外は C, C++ と同じ.
配列は、[ 同じデータ型の多数のデータを集めたもの ]で、個々のデータはインデックス番号で読み書きできる。
REFERENCE ArrayList
要素数が 可変の場合は ArrayList をつかう。
WARNING
[] は読み書きに使うときには変数名の後に付くが、宣言時には変数名の手前になる。
int []a = new int[10];
// int 型要素が 10 個はいる配列インスタンスを a という名前で作成
int[] a = new int[10];
// data を代入して初期化
// { 1, 2... } 初期化子リスト
int[] a = new int[10]{1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
int [] a = { 4, 1, 3, 2, 0 };
// 昇順に Sort
System.Array.Sort( a );
// 要素のインデックスを検索する( なければ -1 )
Array.IndexOf( a, 1 );
// 2次元配列
int[,] a = new int[3,2];
int[,] a = new int[3,2]{ {1,1}, {1,1}, {1,1} };
// 各要素へは , 区切りで指定する
a[0,0] = 1;
// 3次元以上も同じ
int [,,] ar = new int[3,3,3];
Console.Write( ar[0,0,0] );
// 各要素をイテレートする
// IEnumerableというインターフェイスの実装を含む。
foreach( int i in a ) {
}
// 参照型の配列の初期化
Class1 [] ar = new Class1 [] { new Class1(1), new Class1(2), new Class1(3) };
___
■ 参照型の配列
POINT
参照型の配列の場合は, 配列自体の Instance の作成だけでなく,
要素を new した インスタンスをセットする必要がある。
各要素は参照型のため値は null となっている。
利用するには各要素ごとにさらにインスタンスをつくり、その値をセットしておく必要がある。
要素の数が10個なら、配列自身と要素を合わせて、11回のnewをする。
{
// インスタンスを作成。
string [] ar = new string[10];
for( int i=0; i< 10; i++ )
{
// 実行時に null を参照したというエラーになる
//Console.WriteLine( ar[i].Length );
}
for( int i=0; i< 10; i++ )
{
ar[i] = (i*2).ToString();
}
for( int i=0; i< 10; i++ )
{
Console.WriteLine( ar[i].Length );
}
}
{
// Sort の条件は IComparable を実装することで実現できる.
class Item : IComparable
{
public string name;
public int price;
public Item( string name, int price )
{
this.name = name;
this.price = price;
}
// 比較する必要な Interface
// ここでは値段で比較.
public int CompareTo( object obj )
{
return price - ((Item)obj).price;
}
}
class Test
{
static void Main(string[] args)
{
Item [] ar = {
new Item("チョコ",50),
new Item("煎餅",200),
new Item("飴",10),
new Item("ポテチ",100)
};
// Itemクラスのインスタンスをソートする
Array.Sort( ar );
foreach( Item it in ar )
{
Console.WriteLine("{0},{1}",it.name,it.price);
}
}
}
___
■ バイナリ・サーチ
POINT
System.Array クラスには
ソートだけでなくバイナリ・サーチも利用できる。
{
int [] ar = { 4,9,3,6,1,0 };
Array.Sort( ar );
foreach( int n in ar )
{
Console.WriteLine( n );
}
// Array.BinarySearch() で検索可能
int index = Array.BinarySearch( ar, 3 );
Console.WriteLine( "index of 3 is {0}", index );
}
値が“3”である要素のインデックスが“2”であることが分かる。
POINT
Array.BinarySearch() を利用することで, BinarySearch を利用できる。
ただしソートすみである必要がある.
POINT
配列型はれっきとしたデータ型なので、メソッドの引数に使ったり、
メソッドの戻り値として使ったりできる。
配列型は要素数を限定しないため、大きさの決まっていない配列を受け渡すことができる。
{
class Class1
{
static public int [] makeArray( bool select )
{
if( select )
{
int [] ar = { 0,1,2,3 };
return ar;
}
else
{
int [] ar = { 0,1 };
// 配列型をかえす.
return ar;
}
}
// 配列型をうけとる.
// 要素が何個の配列でも受け取ることができる。
static public void dumpArray( int [] ar )
{
foreach( int n in ar )
{
Console.WriteLine( n );
}
}
static void Main(string[] args)
{
int [] ar = makeArray(false);
dumpArray( ar );
Console.WriteLine();
// 配列変数( 参照 )を再度代入することも可能.
// 29 の 配列 Instance は参照がなくなるので, 消えることになる.
ar = makeArray(true);
dumpArray( ar );
}
}
}
要素数に関係なく、配列を受け渡しできる。
POINT
配列変数への代入は, 配列 Instance を再利用しないで, 新規に作成される.
各要素の表示にforeach文を使用すれば、要素の個数を調べる必要はない。
int []が指定されていることから分かるとおり、
このメソッドは、整数の配列を返す。
要素数に関係なく、戻値として渡すことができる。
WARNING
配列変数への代入は、配列の再利用にはならず、
別個の配列インスタンスが生成される。
___
■ 参照型配列の初期化
POINT
参照型の配列も初期化できる
{
class Class1
{
private int n;
public Class1( int param )
{
this.n = param;
}
static void Main(string[] args)
{
for( int i=0; i< 3; i++ )
{
Console.WriteLine( ar[i].n );
}
}
}
}
C#は「コンポーネント指向のプログラム言語」
コンポーネント指向とは、すでに作成済みのクラスを持ち寄って、
それを利用する形でコーディングを行うこと
その際、まったく無関係に開発されたクラスを集めると、同じ名前のクラスが存在することがあり、
名前の衝突が発生する.
C#では、個々のクラスはより大きなnamespace(名前空間)に所属するものと考え、
namespaceを区別することで
同じ名前のクラスを使い分けることを可能とする。
POINT
namespace はネスト可能で . で区切ることができる。
POINT
namespace は複数にわけることもできる。
これによってソースコードを分離することができる。
namespace Space1
{
class Class1
{
public static void test(){}
}
}
namespace Space1
{
class Class2
{
public static void test(){}
}
}
POINT
次にこの namespace 内の名前を使うときに完全修飾名を記述するのは手間なので
using 宣言を利用することで 名前空間の指定を省略できる。
// namespace の省略があった場合に検索する namespace を指定する。
using System.IO;
// 別名をつけることもできる。
using MyClass = Space1.Sub1.Class1;
WARNING
using は、クラスやメソッドの内部には記述できない。
namespace”宣言直後などに記述すること。
namespace を有効にする. {} scope が考慮される.
{
// Block 内のみ有効. System namespace を利用する
using System;
class Foo {}
}
WARNING
ネームスペース直下の 変数宣言はダメ。
必ずクラス内にいれること。
namespace {
Form1 pForm;
}
___
■ namespace と using
namespaceやクラスの別名を宣言するusing
namespaceは便利な機能であるが
正面からこの機能を使うと、
何を指定するにもnamespaceの名前を使うことになり
表記が長くなる。
POINT
ソースコード上の狭い範囲に着目すれば
そこから参照するnamespaceの種類も限られている。
何も長い名前をいちいち書かなくても十分に分かりやすい
ソースを記述できる。
これを支援するために
C#にはusingという機能がある。
POINT
usingには
別名とデフォルトnamespaceを記述する機能がある。
別名機能は、namespaceや、
それに属するクラスに対して、
1つの名前を宣言する機能を提供している。
“ultra.super.special.long.long.name”というような長い
namespaceを頻繁に参照する場合に、もっと短い名前(例えば"longname)を付けることができる。
namespace Space1.Sub1
{
class Class1
{
public static void test()
{
Console.WriteLine("Space1.Sub1.Class1.test() called");
}
}
}
{
// 別名を与えた.
using MyClass = Space1.Sub1.Class1;
using MySpace1 = Space1.Sub1;
using MySpace2 = Space1;
// using Class1 = Space1.Sub1.Class1; // 下のClass1と名前が同じになるのでエラー
// 多重の using はできない.
// using MySpace3 = MySpace2.Sub1; // usingの対象にusingされたキーワードMySpace2は使用できないのでエラー
class Class1
{
static void Main(string[] args)
{
// Space1.Sub1.Class1.test() と同じ.
MyClass.test();
MySpace1.Class1.test();
MySpace2.Sub1.Class1.test();
}
}
}
___
■ ネストするnamespace
POINT
namespace をネストするのと
. 区切りにするのは意味としては同じ。
つまり、namespaceを宣言する際
ピリオド区切りの単一の名前で書いてもよいし、
ネストしたnamespaceとして宣言してもよい。
namespace Space1.Sub1
{
class Class1
{
public static void test()
{
Console.WriteLine("Space1.Sub1.test() called");
}
}
}
namespace Space2
{
namespace Sub1
{
class Class1
{
public static void test()
{
Console.WriteLine("Space2.Sub1.test() called");
}
}
}
}
using System;
public class Test
{
public int v;
}
public class Class1
{
public static int Main(string[] args)
{
// 時間を表示する.
Console.WriteLine( DateTime.Now );
int count = 10000000;
// 1000 万の Instance をつくる.
Test [] test = new Test[count];
for( int i=0; i< count; i++ )
{
test[i] = new Test();
test[i].v = i;
}
int sum = 0;
for( int i=0; i< count; i++ )
{
sum += test[i].v;
}
Console.WriteLine( DateTime.Now );
return 0;
}
}
{
using System;
// struct を利用する.
public struct Test
{
public int v;
}
public class Class1
{
public static int Main(string[] args)
{
Console.WriteLine( DateTime.Now );
int count = 10000000;
Test [] test = new Test[count]; // ここで巨大な Block を確保している.
for( int i=0; i< count; i++ )
{
// struct ならば, ここも不要.
test[i] = new Test();
test[i].v = i;
}
int sum = 0;
for( int i=0; i< count; i++ )
{
sum += test[i].v;
}
Console.WriteLine( DateTime.Now );
return 0;
}
}
}
class Sum
{
private int sum = 0;
public int Add
{
set { sum += value; }
}
public int Result
{
get { return sum; }
}
}
Sum sum = new Sum();
// 代入ではなく足し算が実行される
sum.Add = 1;
private int sum = 0;
// Property
public int Value
{
set { sum = value; }
get { return sum; }
}
Sum sum2 = new Sum();
sum2.Value += 1;
sum2.Value += 2;
// インデクサの添え字を足し算に使用しており
// 定義を全て読まないと挙動がわからない。
public int this[int index]
{
get
{
return a + index;
}
set
{
a = index + value;
}
}
// 内部データと interface となる型を一致させる必要はなし.
private int number;
public string Order
{
get {
switch( number )
{
case 1:
return "one";
case 2:
return "two";
case 3:
return "three";
default:
return "unknown";
}
}
set {
switch( value )
{
case "one":
number = 1;
break;
case "two":
number = 2;
break;
case "three":
number = 3;
break;
default:
number = -1;
break;
}
}
}
___
■ read/writeを制限する
POINT
読み出し専用インデクサ
インデクサを定義する場合はgetとsetの2つの内容を記述するのが普通だが、
getだけ、あるいはsetだけを記述して読み出し専用、書き込み専用のインデクサを作れる。
読み出し専用インデクサへの書き込みはコンパイル・エラーとなる。
class Class2
{
// 宣言 + 初期化.
private char [] a = { 'A', 'B', 'C' };
// Indexer の構文: char を返す.
// Type this[ Type Param ]
// インデクサの宣言は名前には this を入れて、引数は角括弧[]でくくる。
public char this[int index]
{
// get、set、valueのキーワードを用いて記述する。
// 読み出し時の処理を記述する。
get
{
return a[index];
}
set
{
a[index] = value; // 暗黙パラメータ
}
}
}
class Class1
{
static void Main(string[] args)
{
Class2 t = new Class2();
for( int i=0; i< 3; i++ )
{
Console.WriteLine( t[i] );
}
t[0] = 'X';
t[1] = 'Y';
t[2] = 'Z';
for( int i=0; i< 3; i++ )
{
Console.WriteLine( t[i] );
}
}
}
POINT
インデクサによって配列のようにアクセスできるクラスがあるからと言って、
そのクラスが配列の持つ全機能を使えるわけではない。
POINT
インデクサの角括弧[]の内側に記述する値は整数とは限らない
インデクサを使用しているHashtableクラスでは連想配列が可能になる。
Hashtableクラスのインデクサは、読み書きする値も、添え字も、object 型として定義されている。
using System;
using System.Collections;
class Class1
{
static void Main(string[] args)
{
Hashtable h = new Hashtable();
h["斉藤"] = "Windows 2000";
h["田中"] = "Windows 98";
h["鈴木"] = "FreeBSD";
Console.WriteLine( h["斉藤"] );
Console.WriteLine( h["田中"] );
Console.WriteLine( h["鈴木"] );
}
}
[attributes] [modifiers] indexer-declarator {accessor-declarations}
DESC
インデクサを使うと配列でない要素に添字を使用できる。
使いどころとしては簡単なリストを作成し、添字を用いてアクセスしてみる。
ベクトルのように内部で配列を扱うオブジェクトは
内部の配列とオブジェクトの操作をインデクサによって通信させるという方法をとる。
こうすれば
メソッドの仕様を覚える必要なく、クラスの利用者は直感的にプログラムできる。
// こんな感じになるらしい.
T this[int id] { get ; set ; }
オブジェクトを配列のように扱う方法
この配列にアクセスする手段として、インデクサを採用するという設計をとる.
indexer-declarator
type this [formal-index-parameter-list]
インデクサの機能はプロパティの仕様に類似している。
配列のようなオブジェクト操作を暗黙的にメソッドに変換する機能がインデクサ。
インデクサを使いこなすには、一定以上のオブジェクト指向の「設計」経験が必要。
プロパティは名前で識別されるが
インデクサはシグネチャで識別される。
public class list
{
private static int item = 1;
private object value;
private list next;
public list( object o ){
value = o;
next = null;
}
// index を指定して取得
public object get( int index ){
if( index > item - 1 )
return "index error";
list l = this;
while( index != 0 ){
l = l.next;
index--;
}
return l.value;
}
// index を指定してセット
public void set( int index, object value ){
list l = this;
while( index != 0 ){
l = l.next;
index--;
}
l.value = value;
}
// Indexer
// 要は Property の [ index ] version
public object this[int index]{
// 取得する場合は
get{ return this.get(index); }
// セットする場合は,
set{ this.set( index, value ); }
}
// 要素を追加
public void add( object o ){
list l = this;
while( l.next != null ) {
l = l.next;
}
l.next = new list( o );
item++;
}
public void show(){
list l = this;
Console.WriteLine( "item : {0}", item );
do{
Console.WriteLine( l.value );
l = l.next;
}while( l != null );
}
}
{
public static void Main(){
list l = new list( "item1" );
l.add( "item2" );
l.add( 300 );
l.show();
object o = l[0];
l[0] = l[1];
l[1] = o;
l.show();
Console.WriteLine( l[2] );
Console.WriteLine( l[3] );
}
}
class Class2
{
public Class2()
{
Console.WriteLine("public Class2() called");
}
// このコンストラクタの実行に先立って同じクラス内で引数なしのコンストラクタを実行してね ^ ^/ という意味.
public Class2( int x ) : this()
{
Console.WriteLine("public Class2( int x ) called");
}
public Class2( string s ) : this(1)
{
Console.WriteLine("public Class2( string s ) called");
}
}
class ClassBase
{
public int x1,y1;
// base の Ctor
public ClassBase( int x, int y )
{
x1 = x;
y1 = y;
}
}
class ClassDelived : ClassBase
{
public int x2,y2;
// 継承元を呼び出す
public ClassDelived( int x, int y ) : base( x+1, y+1 )
{
x2 = x;
y2 = y;
}
}
class Test {
static void Main() {
// Delegator を作成して, void、パラメータ無しのメソッドの位置を保有する。
TestCallback test = new TestCallback(Test);
// callback を呼ぶ。
test();
}
static void Test() {
System.Console.WriteLine("Test on your lap");
}
}
delegateとは何か
POINT
関数ポインターのこと。
委譲という言葉が分かりにくいと思うなら、「代表者」と考えてもよい。
何かの処理を実行させたいときに、直接処理機能を持つメソッドを呼び出すのではなく、
代表者に処理を求めるのである。代表者は処理機能自体は持っていないが、
それを処理できる適切なメソッドを知っていてそのメソッドに処理要求を渡す。
EventHandler pFoo; といいかえれば OK.
「 クラスA 」の「 メソッドa 」は自分では何も処理せず、
常に「 クラスB 」の「 メソッドb 」に処理をゆだねるとしよう。
このような場合、「 メソッドa 」の中に「 メソッドb 」を呼び出すコードを書けばよいので、
特別な機能は何も必要がない。
A::methodA() {
// ここを固定したくない
B::methodB();
}
POINT
このように常に処理をゆだねる相手が決まっているとは限らないし、
相手が常に1つとも限らない
この状況に対応するために delegate を使う。
WARNING
delegateの機能はeventの機能によく似ている
実際に、event機能はdelegate機能を利用して作られている。
delegateとeventは想定される使われ方が違っており
機能面でも違いがある。
delegateは処理を他にゆだねるものだが
eventはクラス内の出来事を外部に伝えるもの。
どちらか一方だけ覚えて
他方は使わないという方法はうまくいかない。
関数ポインタと似ているのは役割であって
関数ポインタの機能とdelegateの機能が同じではない。
C言語で関数ポインタを使っていた場面で、
C#ではdelegateを使うかもしれないが
具体的な機能はまったく違うということ
// delegate経由でメソッドを呼び出すサンプル
using System;
namespace ConsoleApplication20
{
// delegate 型だよ - ^ ^/
// Signature は int ( int, int ); だよ. ^ ^/
// そして 名前は Sample だよ. ^ ^/
// delegate のデータ型を決める。
// 呼び出せるのは、あくまで同じ戻り値、同じ引数のメソッドのため型を決めておく。
// データ型の名前となる。
// delegate の宣言には、public や private などのキーワードも付けることもできる。
delegate int Sample( int x, int y );
class Class2
{
// Signature が同じであることが POINT
public int method( int x, int y )
{
return x*y;
}
}
class Class1
{
static void Main(string[] args)
{
Class2 instance = new Class2();
// 代理人を作成する際に, Method をもらう. ( Isntance つき. )
// delegate を作成して instance のメソッドを渡す。
Sample sample = new Sample( instance.method );
// 代理人経由で呼び出す.
int result = sample( 2, 3 );
Console.WriteLine( result );
}
}
}
POINT
delegate を使って異なる処理が一元化できる。
using System;
namespace ConsoleApplication21
{
delegate int Sample( int x, int y );
class Class2
{
public int methodMult( int x, int y )
{
return x*y;
}
public int methodPlus( int x, int y )
{
return x+y;
}
}
class Class1
{
// delegate 型を外からもらうことが可能.
// 計算方法を外へ出すということ。
public static void calc( int x, int y, Sample calcMethod )
{
// calcメソッドで、さまざまな計算を実行させたい
// このdelegateのインスタンスの移譲先として指定するメソッドを入れ替えれば
計算内容も変わる
int result = calcMethod( x, y );
Console.WriteLine( result );
}
static void Main(string[] args)
{
Class2 instance = new Class2();
calc( 2, 3, new Sample( instance.methodMult ) );
calc( 2, 3, new Sample( instance.methodPlus ) );
}
}
}
___
■ Partial
SYNTAX
partial class 型名 { ... }
DESC
ひとつの長い Class を分割して記述できる. ( コンパイラがあとで結合してくれる. )
Partial types は、一つのクラス、構造体、インタフェースを複数のファイルなどに分割して記述することができる機能。
Form の GUIHandler に対して有効に使われる。
Form のハンドラーで自動生成されるコードは Partial により別ファイルにできる。
これはユーザ自身のコードと混ざって間違って編集しないようにするため。
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
// 閉じる.
Close();
}
}
POINT
使いどころは
GUI プログラムにおいて、Form を継承したメインウィンドウを担当するクラスを作成するとき。
CallbackMethod が大量に必要になるため.
極めてコード量が多くて管理が難しかったクラスなどは、partial によって複数のクラスに分割するなどして
管理しやすくなる。
partial class Test {
static void Main() {
System.Console.WriteLine(new Test());
}
}
partial class Test {
public override string ToString() {
return "Test";
}
}
___
■ Interface
POINT
インターフェイスは単なるプログラム言語の機能というだけでなく、
システム側に用意されたさまざまな便利な機能にアクセスするための入り口という意味合いも持つ。
いろいろなインターフェイスを自作クラスに実装することにより、システムが用意したさまざまな機能を利用したり、
自由自在にコントロールしたりできる。
そういう意味で、インターフェイスを自分で宣言するよりも、
まずは既存のインターフェイスの活用から始めるとよいだろう。
___
■ インターフェイスの効能
POINT
Interface と Inheritance の違いは, Interface は複数可能だが, Inheritance はひとつのみ.
その制約が生まれた背景は, MultiInheritance が Trouble をひきおこしやすいため.
ので Inheritance はひとつ.Interface は複数という Rule にした.
逆に C++ ではこの Guideline にあわせよう. ^ ^/
使いどころとしては,
Inheritance : 機能性を継承.
Interface : クラスの種類に関係なく呼び出し方法を合わせる.
初心者の場合、自分でインターフェイスを定義する機会は多くないかも。
しかし、システム側で定義しているいくつかのインターフェイスを自作クラスに実装すると、
いろいろと便利な機能が有効になる
[ システムが定義するインターフェイス ]を活用すると何ができるのかを紹介しよう。
-> 自分が System になって Interface を定義することも可能. ^ ^/
インターフェイスの最も基本的な効能
継承関係がまったくないクラスであるClass2とClass3を定義している。
この2つのクラスには、同じtaskという名前のメソッドがある。
ある外部のメソッドから、Class2とClass3の違いに関係なく、taskメソッドを呼び出したいとしよう。
// インターフェイスを用いて解決した例
using System;
namespace ConsoleApplication93
{
// Interface を作成. ( Access ないのね. ^ ^/ )
interface ISpecialTask
{
void task();
}
class Class3 : ISpecialTask
{
public void task()
{
Console.WriteLine("task() in Class3");
}
}
class Class1
{
static void callTask( ISpecialTask ist )
{
ist.task();
}
static void Main(string[] args)
{
Class2 c2 = new Class2();
Class3 c3 = new Class3();
callTask( c2 );
callTask( c3 );
}
}
}
インターフェイスの最も基本的な効能を示すサンプル・プログラム1
異なるクラスにある同じ名前のメソッドを、クラスの違いとは無関係に呼び出す。
通常は継承関係もないクラス間で、共通のメソッドを持つことはできない。
無関係なクラス間で共通のメソッドを持つには、インターフェイスを共有する、
という方法がある。
Class2とClass3は、共通のインターフェイス ISpecialTask を実装している。
ISpecialTaskはtask()というメソッドを実装すべきことを示す。
ここではメソッドの内容は記述されない。あくまで、引数と戻り値だけが指定される。
それぞれのクラスが、このインターフェイスを実装することを明示的に指定している。
callTaskメソッドは、引数としてISpecialTaskインターフェイスへの参照を取るように記述されている。
ISpecialTaskを実装したクラスのインスタンスからは、必ずこのインターフェイスへの参照を取得できる。
つまり、Class2のインスタンスからも、Class3のインスタンスからも取得できることになる。
そのため、33〜34行目のように、どちらのクラスのインスタンスも、引数に渡すことができる
(厳密にいえば、インスタンスへの参照から、インターフェイスへの参照が取り出されて
それがメソッドに渡る)。
上の例は、インターフェイスを使わなくても、継承を使って実現できる。
using System;
namespace ConsoleApplication94
{
// 抽象クラスをつくる.
abstract class ClassSpecialTask
{
public abstract void task();
}
class Class2 : ClassSpecialTask
{
public override void task()
{
Console.WriteLine("task() in Class2");
}
}
class Class3 : ClassSpecialTask
{
public override void task()
{
Console.WriteLine("task() in Class3");
}
}
class Class1
{
static void callTask( ClassSpecialTask ist )
{
ist.task();
}
static void Main(string[] args)
{
Class2 c2 = new Class2();
Class3 c3 = new Class3();
callTask( c2 );
callTask( c3 );
}
}
}
POINT
インターフェイスを1つだけ実装する場合は
継承を用いても、ほぼ同等の機能を実現できる。
しかし
実装するインターフェイスが2つになると、継承で類似機能を記述することはできなくなる。
以下は、クラスが2個のインターフェイスを実装している例である。
using System;
namespace ConsoleApplication95
{
interface ISpecialTask1
{
void task1();
}
interface ISpecialTask2
{
void task2();
}
class Class2 : ISpecialTask1, ISpecialTask2
{
public void task1()
{
Console.WriteLine("task1() in Class2");
}
public void task2()
{
Console.WriteLine("task2() in Class2");
}
}
class Class3 : ISpecialTask1, ISpecialTask2
{
public void task1()
{
Console.WriteLine("task1() in Class3");
}
public void task2()
{
Console.WriteLine("task2() in Class3");
}
}
class Class1
{
static void callTask1( ISpecialTask1 ist )
{
ist.task1();
}
static void callTask2( ISpecialTask2 ist )
{
ist.task2();
}
static void Main(string[] args)
{
Class2 c2 = new Class2();
Class3 c3 = new Class3();
callTask1( c2 );
callTask1( c3 );
callTask2( c2 );
callTask2( c3 );
}
}
}
継承では類似の機能を記述することはできない。
2つのクラスにある2つのメソッドが、それぞれ順に呼び出される。
インターフェイスも継承も、ソースコード上では同じように見える。
クラス宣言のクラス名の後に、コロン記号(:)を書いて、
その後にインターフェイスか継承するクラス名を記述する。
だが、両者の間で決定的に違うのは、インターフェイスの名前は何個でも記述できるのに対して、
継承するクラス名は1個しか記述できないこと。
このような制限は、技術的なものではない。
事実C++では、継承するクラス名をいくつ書いてもよい。
これを多重継承という。多重継承ができれば、インターフェイスはなくてもよい。
事実、C++ にインターフェイスはない。
にもかかわらず、C#やJavaといった比較的新しい言語で多重継承が禁止されているのは、
継承が過剰に強力すぎる機能であり、トラブルを引き起こしやすいという経験による。
強力すぎる継承の機能を、適正な水準まで制限した結果が、継承するクラスは1個までという制約と、
インターフェイスの導入である。
POINT
機能性を継承するときは継承を使う。
呼び出し方法を合わせるときはインターフェイスを使う
{
public class Class1
{
public static int Main(string[] args)
{
int a=1;
{
int b=2;
Console.WriteLine(a);
Console.WriteLine(b);
}
Console.WriteLine(a);
// Console.WriteLine(b); // エラーになる
return 0;
}
}
}
// 元の値(123)がそのまま各変数に引き継がれる。
{
sbyte b = 123;
short s = b;
int i = s;
long l = i;
Console.WriteLine("{0},{1},{2},{3}",b,s,i,l);
return 0;
}
{
long l = 123;
int i = l;
short s = i;
sbyte b = s;
Console.WriteLine("{0},{1},{2},{3}",b,s,i,l);
return 0;
}
キャストを使用して、自身よりも表現力の小さい変数に値を代入した。コンパイル・エラーも発生しないし、
POINT: Cast( 型変換 )は Data を壊す場合がある.
キャストがデータを壊すとき
上記の例は、扱う数値がどのデータ型にも収まるほど小さかったので、問題なく処理できた。
いつも数値が十分に小さいわけではない。
桁数が多いデータを、それが収まりきらないほど桁数が小さいデータ型の変数に入れてしまう場合もあるだろう。
キャストを使えばそれは可能である。
桁数が足りない以上、元の値を保つことが不可能である。
{
using System;
public class Class1
{
public static int Main(string[] args)
{
long l = 1844674407370955161;
int i = (int)l;
short s = (short)i;
sbyte b = (sbyte)s;
Console.WriteLine("{0},{1},{2},{3}",b,s,i,l);
return 0;
}
}
}
正数だったはずが負数扱いされてしまう場合もある。
格納不能な数値をキャストによって強制的に変換して代入したため
数値が破壊されてしまった。
POINT
bitiamge では 切り詰める作業をしているだけ. -> しかし整数としてみると,まったく異なる値になる.
10進数ではバラバラの値に見えるが
内部的にはビットイメージの桁数を切りつめる処理が行われているだけ
しかし、10進数として見るとまったく予測できない値に変化する。
あくまで10進数の値が重要である場合には、値が大きく狂うような状況が発生しないようにプログラミングしなければならない。
キャストをなるべく使わないように書けば
このような問題を自動的に回避できる。
キャストしなければならない必然性が特にない場所でキャストを多用するようなソースコードは
潜在的にバグが入り込む可能性が大きい。
POINT
だから、事前に何桁の数値が必要になるかを見積もった上で
使うデータ型を統一し、キャストをできるだけ少なくするようにプログラミングすること。
___
■ 実数でもキャスト
データ変換は、整数だけでなく、実数でも必要とされる。
C#には、桁数の多いdouble型と桁数の少ない
float型という2種類の浮動小数点実数のデータ型がある。
桁数の多いdouble型の値を桁数の少ないfloat型に代入する場合は
整数と同じくキャストが必要。
{
double d = 0.123456789012345;
float f = (float)d;
Console.WriteLine("{0},{1}",d,f);
return 0;
}
POINT
double型をfloat型にキャストすると、数値の下の桁が切り捨てられていることが分かる。
前出の整数の場合のように、とんでもない値になってしまうわけではない。
桁数の少ないデータ型に変換されたときに切り捨てられるのは下の桁である。
値のおおまかな値は変化しない。精度が落ちるだけ。
その意味で、整数をキャストする場合に比べれば比較的被害は少ない。
ちなみに、上記画像の最後が“91”で終わっているが、変換元の値を見れば、ここは“89”になるべき場所である。このような食い違いが生じるのは、
2進数の浮動小数点演算が、10進数の表記を正確に表記できないことによって起こる誤差であり、原理的に不可避である。
これを避けるには、2進数ではなく10進数で処理するdecimal型を用いるが、処理速度や効率はfloat型やdouble型の方が勝る。
定数値にキャスト
誤差の出ない計算を行うなら、decimal 型が有利。
{
using System;
public class Class1
{
public static int Main(string[] args)
{
decimal e = 0.123456789;
Console.WriteLine("{0}",e);
return 0;
}
}
}
{
using System;
public class Class1
{
public static int Main(string[] args)
{
short i = -1;
ushort u = (ushort)i;
// A: -1, 65536
Console.WriteLine("{0},{1}",i,u);
return 0;
}
}
}
負の数を符号なしのデータ型に代入するプログラム
POINT
符号つき変数は( CMP くんの )解釈が異なる.
ビットとしての情報は失われていないものの、符号付き変数では値の解釈が異なるため、
POINT:
bit Level の情報は失われていない.
変数iと変数uは、機械語レベルで格納されるビットの値としてはまったく同一であり、
ビット単位で処理する演算子を用いるとまったく同じ結果になる。
しかし、[ 数値としての値 ]は同じではないことを、肝に銘じておく必要がある。
言い換えれば、符号付きと符号なしのデータ型は違うものだと認識して、
混用はなるべく避ける方がよい。
POINT
整数型は少数を表現できない
切り捨ては同一符号 && 絶対値が超えない値になる
小数の切り捨て
整数型には小数を表現できない。
キャストによって実数型から整数型に変換させることはできるが、
小数点以下の値はすべて失われる。
{
for( float f=-2; f< 2; f += 0.25f )
{
int i = (int)f;
Console.WriteLine("{0},{1}",f,i);
}
}
POINT
浮動小数点数値を整数型変数にキャストした場合には、
同一符号で絶対値が本来の値(浮動小数点値)を超えない最大の整数値に変換される。
WARNING
四捨五入に慣れたVisual BASICプログラマなどは要注意
___
■ 参照型のキャスト
クラスからnewによって作成したインスタンスも、キャストの対象になる。
ソース内で宣言したクラスであるClass1のインスタンスをobject型にキャストしている。
object型は、すべてのクラスに対する共通の基底となるクラスなので
すべてのクラスのインスタンスは、object型にキャストできる。
{
using System;
public class Class1
{
public String hello;
public static int Main(string[] args)
{
Class1 c = new Class1();
c.hello = "Hello!";
object o = (object)c;
Class1 c2 = (Class1)o;
Console.WriteLine("{0},{1}",c.hello,c2.hello);
return 0;
}
}
}
一度object型にキャストしたものを、
もう1度キャストして元のClass1型に戻している。
もし、object型にキャストされたときに、
インスタンスがobject型に変換されているなら
Class1の内部で宣言したhelloという変数はその時点で失われるはず。
そのため、再びClass1型にキャストしても
helloに代入した“Hello!”という文字列は失われているはず。
POINT
参照型の Cast は元に戻せば, 元のクラスの Instance として利用可能.
キャストによってデータが失われたら永遠に戻らない整数型などの値型と、
クラスなどの参照型の違いである
このあたりが C と通ずる。
参照型は、object型などのより基本的な型にキャストしても、
元の型に再キャストすれば、完全に元どおりの機能を取り戻す。
{
using System;
public class Class2
{
public String hello;
}
public class Class1
{
public String hello;
public static int Main(string[] args)
{
Class1 c = new Class1();
c.hello = "Hello!";
object o = (object)c;
Class2 c2 = (Class2)o;
Console.WriteLine("{0},{1}",c.hello,c2.hello);
return 0;
}
}
}
{
using System;
public class Class2
{
public String hello;
}
// 継承した.
public class Class1 : Class2
{
//public String hello;
public static int Main(string[] args)
{
Class1 c = new Class1();
c.hello = "Hello!";
object o = (object)c;
Class2 c2 = (Class2)o;
Console.WriteLine("{0},{1}",c.hello,c2.hello);
return 0;
}
}
}
{
public class Class1
{
public static int Main(string[] args)
{
int i = 123;
object o = i;
int j = (int)o;
Console.WriteLine("{0},{1},{2}",i,o,j);
return 0;
}
}
}
public class IntFixedArray {
public IntFixedArray( int num ) {
_num = num;
_data = new int[_num];
}
public void Set( int i, int value ) { _data[i] = value; }
public int Get( int i ) { return _data[i]; }
int[] _data;
int _num;
}
public class FixedArray {
public FixedArray( int num ) {
_num = num;
_data = new object[_num];
}
public void Set( int i, object value ) { _data[i] = value; }
public object Get( int i ) { return _data[i]; }
object[] _data;
int _num;
}
public class FixedArray< T> {
public FixedArray( int num ) {
_num = num;
_data = new T[_num];
}
public void Set( int i, T value ) {
_data[i] = value;
}
public T Get( int i ) {
return _data[i];
}
T[] _data;
int _num;
}
FixedArray< int> a = new FixedArray< int>(10);
a.Set( 0, 1 );
// コンパイル時に型チェックもしてもらえる。
a.Set( 0, "foo" );
FixedArray a = new FixedArray(10);
a.Set( 0, "foo" );
// ここで 値型から参照型への変換( boxing )が入る。
a.Set( 0, 1 );
___
■ Image
___
■ bitmap
参照の追加
System.Drawing
using System.Drawing;
using System.Drawing.Imaging;
{
// Bitmapオブジェクト作成
Bitmap b = new Bitmap("d:/test.bmp");
// Colorの変更
for (int i = 0; i < b.Height; i++)
{
for (int j = 0; j < b.Width; j++)
{
Color c = Color.FromArgb(255, b.GetPixel(j, i).R, b.GetPixel(j, i).G, 255);
b.SetPixel( j, i, c );
}
}
// ポインタ経由で直接変更をする。
unsafe
{
BitmapData data = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
// ピクセルデータの開始アドレス
IntPtr pt = data.Scan0;
byte *p = (byte *)pt;
for( int i=0; i< b.Width*b.Height; i++ ){
p[i] = 128;
}
}
// セーブ
b.Save("d:/test_out.bmp");
// PNG形式に変換して保存
b.Save( "d:/test_out.png", System.Drawing.Imaging.ImageFormat.Png );
}