鄭士康 國立台灣大學 電機工程學系/電信工程研究所/ 資訊網路與多媒體研究所 物件與類別 鄭士康 國立台灣大學 電機工程學系/電信工程研究所/ 資訊網路與多媒體研究所
封裝(Encapsulation) 類別將狀態變數與功能函式封裝起來 通常狀態變數為私有(private) ,只供同類別之功能函式取用,不可以由類別外直接取用 避開同名變數問題 避免被不慎改動,破壞狀態的正確一致性 容易維護,不影響外界程式 功能函式可能公用(public) ,也可能私有 一類別至少有一公用函式,以便應用
類別Dice
新增類別程式 專案>加入類別>類別 輸入類別名稱.cs
DiceSimulation.Program片段 Dice dice = new Dice(); dice.Toss(); Console.WriteLine( "擲出"+dice.FaceValue );
DiceSimulation.Dice 片段 public class Dice { int faceValue = 1; Random rand = new Random(); public int FaceValue get { return faceValue; } set { faceValue = value; } } public void Toss() faceValue = rand.Next() % 6 + 1;
程序( Procedure )呼叫 *J. G. Brookshear, Computer Science – An Overview, 8th edition, Addison-Wesley, 2005
函式呼叫流程 static void Main(string[] args) { public class Dice { . . . public void Toss() faceValue = rand.Next() % 6 + 1; } dice.Toss(); } public class Random { . . . public int Next() }
資料型別 實值型別(Value Type) 參考型別(Reference Type) 結構(Structs) 數值(Numeric) 布林(bool) 使用者定義Structs 列舉(Enumerations) 參考型別(Reference Type) 字串(string) 物件(object)
堆疊(Stack)與堆積(Heap) Heap . Stack
實值型別儲存方式 堆疊(Stack) int x = 100; x 100
參考型別儲存方式 堆積(Heap) 堆疊(Stack) string x = “abc”; x 參考 ‘a’ ‘b’ ‘c’
物件(Object)觀念 dice1 dice1.faceValue 記憶體地址1 dice1.rand 函式Toss進入地址 dice2 記憶體地址2 dice2.rand 函式Toss進入地址 函式成員Toss進入點
物件的設定(Assignment) dice2 = dice1; dice1 dice1.faceValue 記憶體地址1 dice1.rand 函式Toss進入地址 dice2 dice2 = dice1; dice2.faceValue 記憶體地址2 dice2.rand 函式Toss進入地址 函式成員Toss進入點
存取修飾詞 public private protected internal
偵錯器的進一步應用 “逐步執行”與”不進入函式”的差別 目前類別與函式顯示 監看式的應用 呼叫堆疊的應用 即時運算視窗的應用
練習 宣告並測試以下類別
測試 軟體錯誤代價可能極為高昂 測試可以發現並改正程式錯誤 通過的測試越多, 程式越可靠
測試時機與風險
極端化程式設計 (Extreme Programming) 撰寫一個簡單的測試程式 編譯執行程式,看它失敗 增添恰能通過測試之程式碼並編譯偵錯 重整及消除冗餘程式碼並編譯偵錯 反覆進行上述步驟
測試驅動開發方式 (Test-Driven Development -TDD) 寫測試主程式, 呼叫測試函式, 建置, 看它失敗 寫測試類別, 加上測試函式, 重新建置, 看它失敗 撰寫欲測試的類別, 加上株段(stub)函式, 使通過建置, 但不通過測試 改寫株段函式, 通過建置與測試 以相同方式, 增加新測試
TDD 參考資料 P. Provost, “Test-driven development in .NET,” http://www.codeproject.com/KB/dotnet/tdd_in_dotnet.aspx
類別 Card
測試規畫 ♠A ♥J ♦10 ♣3
程式TestingCard.Program.cs using System; using System.Diagnostics; namespace TestingCard { class Program static void Main(string[] args) CardTest test = new CardTest(); Debug.Assert(test.Spade_A_OK()); }
程式TestingCard.CardTest.cs using System; namespace TestingCard { class CardTest public bool Spade_A_OK() Card card = new Card(); card.Suit = 's'; card.Rank = 1; return (card.Suit == 's' && card.Rank == 1); }
程式TestingCard.Card.cs using System; namespace TestingCard { public class Card { private char suit; private int rank; public char Suit { get { return suit; } set { suit = value; } } public int Rank { get { return rank; } set { rank = value; }
練習 完成類別Card的其他三個測試
練習 重新以TDD測試類別Car
CalculatorTest.Program片段 Calculator calculator = new Calculator(); Console.Write("輸入第一個數字: "); operand1 = int.Parse(Console.ReadLine()); Console.Write("輸入第二個數字: "); operand2 = int.Parse(Console.ReadLine()); result = calculator.Add(operand1,operand2); Console.WriteLine("{0} + {1} = {2} ", operand1, operand2, result);
CalculatorTest.Calculator 片段 public class Calculator { public int Add(int a, int b) int result = a + b; return result; } public int Subtract(int a, int b) int result = a - b; . . .
程序參數與區域變數 模組(Module)觀念 參數(Parameters, 或稱引數 Arguments) 程式碼重覆使用 程式共同開發 資料獨立 參數(Parameters, 或稱引數 Arguments) 形式參數(Formal Parameters) 真實參數(Actual Parameters) 區域變數(Local variables)
數學函數呼叫流程 double y = Math.Sqrt( 2.0 ); class Math { static double Sqrt( double x ) . . . return result; }
記憶配置 呼叫模組記憶區 區域變數 真實參數 結果變數 形式參數 被呼叫模組記憶區 區域變數 傳回值
函式傳回值 Program.Main() result calculator.Add() result return value
練習 設定operand值並利用Assert敘述改寫CalculatorTest程式
SwappingIntegers.Program 片段 int x = 3; int y = 5; Swap(ref x, ref y); Debug.Assert(x == 5 && y == 3); . . . static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; }
PassByReferenceAndOut.Program片段(1/2) double length = 100.0; Square s = new Square(); s.Side = length; double area1 = 0.0; double perimeter1 = 0.0; s.GetAreaAndPerimeter(ref area1, ref perimeter1); Console.WriteLine( "正方形邊長: {0}, 面積: {1}, 周長: {2}", length, area1, perimeter1);
PassByReferenceAndOut.Program片段(2/2) double area2; double perimeter2; s.GetAreaAndPerimeterUsingOut(out area2, out perimeter2); Console.WriteLine( "正方形邊長: {0}, 面積: {1}, 周長: {2}", length, area2, perimeter2);
PassByReferenceAndOut.Square 片段 public void GetAreaAndPerimeter( ref double area, ref double perimeter) { area = Math.Pow(a, 2); perimeter = 4.0 * a; } public void GetAreaAndPerimeterUsingOut( out double area, out double perimeter)
傳值, 傳址, out 參數 傳值參數傳遞( Pass by value ) 傳址參數傳遞( Pass by reference ) 初值設定問題
練習(1/3) 寫主程式Program.Main() 以傳值參數參照下一頁虛擬碼,寫一個函式Program.Swap()交換兩張牌位置 利用之前的Card類別,宣告兩張撲克牌Card物件 呼叫Program中的static函數Swap()交換兩張牌位置 以傳值參數參照下一頁虛擬碼,寫一個函式Program.Swap()交換兩張牌位置
練習(2/3) 函式Swap虛擬碼 Procedure Swap( card1, card2 ) 1. temp = card1; return card1 temp card2
練習(3/3) 將Swap函式之參數設為傳值參數 用Assert敘述測試兩張牌能否交換 將Swap函式之參數改為傳址參數,再試一次 利用偵錯器找出結果差異之原因
OverloadingDemo.Program 片段 Adder adder = new Adder(); int a = 3; int b = 5; Debug.Assert(adder.Add(a, b) == 8); double ad = 3.2; double bd = 5.1; Debug.Assert(adder.Add(ad, bd) == 8.3);
OverloadingDemo.Adder 片段 public int Add(int a, int b) { return (a + b); } public double Add(double a, double b)
DiceSimulation2.Program 片段 int seed = 123; Dice dice = new Dice(seed); dice.Toss(); Console.WriteLine( "擲出" + dice.FaceValue );
DiceSimulation2.Dice片段 public Dice() { rand = new Random(); Toss(); } public Dice(int seed) rand = new Random(seed);
建構函式與解構函式 (Constructor and Destructor) 預設建構函式(default constructor) 具參數之建構函式 檢驗參數範圍 解構函式
物件產生與消滅流程 static void main( string[] arg ) { public Dice(int seed) { . . . } Dice dice = new Dice( seed ); 物件宣告 物件生成 ~Dice() { . . . } } 刪除物件dice 離開主函式, 程式結束
練習 修改類別Card,改用建構函式設定初值,以屬性取得suit及rank
UsingThis.Program 片段 Time t = new Time(11, 30, 52); int hour; int min; int sec; t.GetTime(out hour, out min, out sec); Console.WriteLine( "現在時間{0} : {1} : {2}", hour, min, sec);
UsingThis.Time片段 public Time(int hour, int min, int sec) { bool paramsAreValid = hour >= 0 && hour < 24 && min >= 0 && min < 60 && sec >= 0 && sec < 60; if( paramsAreValid ) { this.hour = hour; this.min = min; this.sec = sec; } else { Console.WriteLine("Time建構式參數值不合理"); }
物件自我參考 this t 記憶體地址 hour min sec this GetTime()進入地址 函式成員GetTime進入點
類別TimeConversion
UsingStatic.Program片段 int hoursToMins = TimeConversion.HoursToMins(hours); int daysToHours = TimeConversion.DaysToHours(days); Console.WriteLine(hours + " hours = " + hoursToMins + " minutes"); Console.WriteLine(days + " days = " + daysToHours + " hours"); Test t1 = new Test(); Test t2 = new Test(); Console.WriteLine(Test.GetNConstructed + " Test objects were constructed");
UsingStatic.TimeConversion片段 public static class TimeConversion { private const int HOURS_PER_DAY = 24; private const int MINS_PER_HOUR = 60; public static int HoursToMins( int hours ) return hours*MINS_PER_HOUR; } public static int DaysToHours( int days ) return days*HOURS_PER_DAY;
UsingStatic.Test片段 public class Test { private static int nConstructed = 0; public Test() { ++nConstructed; } public static int GetNConstructed { get { return nConstructed; }
靜態成員與靜態類別 常數宣告 靜態成員應用場合 靜態函式Main 靜態類別應用場合
靜態成員的記憶配置 t1 記憶體地址1 函式Test()進入地址 t2 函式Test()進入地址 記憶體地址2 函式成員Test進入點 nConstructed 記憶體地址 函式成員 GetNConstructed進入點
練習 (1/2) 在類別Card內增加靜態成員函式,累計產生的Card物件數 寫一程式利用類別Card產生三張撲克牌,放在deck內,印出產生的牌數
練習 (2/2) 實作並測試一靜態類別EqSolver,內含兩靜態成員函式double Linear(double a, double b)及double Quadratic(double a, double b, double c)分別解一次方程式a x + b = 0及a x2 + b x + c = 0
UsingStruct.Program 片段 (1/2) struct Point2D { public int x; public int y; }
UsingStruct.Program 片段 (2/2) static void Main(string[] args) { Point2D pt = new Point2D(); Console.WriteLine( "Initial location = ({0},{1})", pt.x, pt.y); pt.x = 3; pt.y = 4; "Final location = ({0},{1})", pt.x, pt.y); }
StructVSClass.SPoint2D 片段 struct SPoint2D { public int x; public int y; public SPoint2D(int x, int y) this.x = x; this.y = y; }
StructVSClass.CPoint2D 片段 { public int x; public int y; public CPoint2D() x = 0; y = 0; } public CPoint2D(int x, int y) this.x = x; this.y = y;
StructVSClass.Program片段 (1/4) SPoint2D sPt1 = new SPoint2D(3, 4); SPoint2D sPt2 = new SPoint2D(); SPoint2D sPt3 = new SPoint2D(); sPt2 = sPt1; sPt3 = sPt1; Console.WriteLine("sPt1 = ({0}, {1})", sPt1.x, sPt1.y); Console.WriteLine("sPt2 = ({0}, {1})", sPt2.x, sPt2.y); Console.WriteLine("sPt3 = ({0}, {1})", sPt3.x, sPt3.y);
StructVSClass.Program 片段(2/4) CPoint2D cPt1 = new CPoint2D(3, 4); CPoint2D cPt2 = new CPoint2D(); CPoint2D cPt3 = new CPoint2D(); cPt2 = cPt1; cPt3 = cPt1; Console.WriteLine("cPt1 = ({0}, {1})", cPt1.x, cPt1.y); Console.WriteLine("cPt2 = ({0}, {1})", cPt2.x, cPt2.y); Console.WriteLine("cPt3 = ({0}, {1})", cPt3.x, cPt3.y);
StructVSClass.Program片段 (3/4) sPt1.x = 10; sPt1.y = 20; sPt2.x = 30; sPt2.y = 40; Console.WriteLine("sPt1 = ({0}, {1})", sPt1.x, sPt1.y); Console.WriteLine("sPt2 = ({0}, {1})", sPt2.x, sPt2.y); Console.WriteLine("sPt3 = ({0}, {1})", sPt3.x, sPt3.y);
StructVSClass.Program片段 (4/4) cPt1.x = 10; cPt1.y = 20; cPt2.x = 30; cPt2.y = 40; Console.WriteLine("cPt1 = ({0}, {1})", cPt1.x, cPt1.y); Console.WriteLine("cPt2 = ({0}, {1})", cPt2.x, cPt2.y); Console.WriteLine("cPt3 = ({0}, {1})", cPt3.x, cPt3.y);
結構 定義方式與類別相似 記憶配置於堆疊 適合使用於小型資料集合 減少記憶回收之負擔 不可自訂預設建構函式 成員變數不能直接設定初值 設值時直接複製資料成員內容 不支援繼承功能
類別物件記憶配置 cPt2 = cPt1; cPt1 cPt1.x 記憶體地址1 cPt1.y cPt2 cPt2.x 記憶體地址2 heap space
結構物件記憶配置 sPt1.x sPt1 sPt1.y sPt2 sPt2.x sPt2.y stack
練習 宣告並測試結構Student,其中包括學號、姓名、成績
UsingCopyConstructor.CPoint2D片段 public CPoint2D() { x = 0; y = 0; } public CPoint2D(int x, int y) this.x = x; this.y = y; public CPoint2D(CPoint2D p) x = p.x; y = p.y;
UsingCopyConstructor.Program片段 (1/2) CPoint2D cPt1 = new CPoint2D(3, 4); CPoint2D cPt2 = new CPoint2D( cPt1 ); CPoint2D cPt3 = new CPoint2D( cPt1 ); Console.WriteLine("cPt1 = ({0}, {1})", cPt1.x, cPt1.y); Console.WriteLine("cPt2 = ({0}, {1})", cPt2.x, cPt2.y); Console.WriteLine("cPt3 = ({0}, {1})", cPt3.x, cPt3.y);
UsingCopyConstructor.Program片段 (2/2) cPt1.x = 10; cPt1.y = 20; cPt2.x = 30; cPt2.y = 40; Console.WriteLine("cPt1 = ({0}, {1})", cPt1.x, cPt1.y); Console.WriteLine("cPt2 = ({0}, {1})", cPt2.x, cPt2.y); Console.WriteLine("cPt3 = ({0}, {1})", cPt3.x, cPt3.y);
淺層複製與深層複製 淺層複製(Shallow copy) 深層複製(Deep copy) 系統提供 只複製參考(Reference)地址 沒有新物件產生 深層複製(Deep copy) 程式師提供 產生新物件 應複製所有資料成員
練習 宣告並測試類別Student,其中包括學號、姓名、成績,仿照程式StructVSClass及UsingCopyConstructor分別使用設值與複製建構函式產生Student物件,觀察淺層複製與深層複製的差別
株段函式(Stub Functions)與 混充類別(Mock Classes) 初期測試用以確認呼叫方式正確 多數程式錯誤發生於函式呼叫 易寫 易建立 執行快速 產生確定結果 易確認呼叫方式正確
BlackJack_0_0_0.Program using System; using System.Diagnostics; namespace BlackJack_0_0_0 { class Program static void Main(string[] args) Debug.Assert( BlackJackTest.Scenario_1_OK()); }
混充類別BlackJack_0_0_0.BlackJackTest using System; namespace BlackJack_0_0_0 { class BlackJackTest public static bool Scenario_1_OK() return true; }
專案與方案 專案( Project ) 一個完整的應用程式或程式庫 方案( Solution ) 集合幾個專案的問題解決方案
二十一點遊戲模擬原型方案規畫 方案:BlackJack_0_0_1 專案:BlackJack_0_0_1 專案:TestingCard2 Program, BlackJackTest, Deck, HumanPlayer, ComputerPlayer, Hand, Card 專案:TestingCard2 Program, CardTest
BlackJack_0_0_1.Card 片段(1/2) public enum Suit { CLUB = 0, DIAMOND = 1, HEART = 2, SPADE = 3 }
BlackJack_0_0_1.Card 片段(2/2) public struct Card { public Suit suit; public int rank; public Card(Suit suit, int rank) this.suit = suit; this.rank = rank; }
新增專案TestingCard2 方案總管>(方案BlackJack_0_0_1)右鍵>加入>新增專案>儲存現有專案>專案命名(TestingCard2) 方案總管>(專案TestingCard2)參考>右鍵>加入參考>專案>(BlackJack_0_0_1) 撰寫主程式TestingCard2.Program 建立並實作類別CardTest 建置專案TestingCard2 設定專案TestingCard2為啟始專案 開始偵錯
TestingCard2.Program using System; using System.Diagnostics; namespace TestingCard2 { class Program static void Main(string[] args) Debug.Assert(CardTest.Spade_A_OK()); }
TestingCard2.CardTest using System; using BlackJack_0_0_1; namespace TestingCard2 { class CardTest public static bool Spade_A_OK() Card card = new Card(Suit.SPADE, 1); return (card.suit == Suit.SPADE && card.rank == 1); }
二十一點遊戲模擬v0.1流程 產生牌疊 電腦(莊家)向玩家(一人)及本身派發一張明牌 電腦向玩家及本身派發一張明牌 莊家詢問玩家是否加牌, 直至玩家不加牌或報到 莊家如不足 17點便需加牌直至超過或等於 17點 對未有爆煲或報到的玩家, 比點數大小, 大者勝, 如莊家爆煲, 玩家勝
二十一點遊戲模擬原型之測試規畫: 場景1 玩家 莊家 ♠A ♥J ♦10 勝
UML 類別圖( Class Diagram ) 92
Scenario 1: Sequence Diagram
Stepwise Refinement 演算法設計 Magic number 7 加減 2 逐層分解工作 各項工作依繁簡、重複性、可替代性決定是否寫為函式
IsBlackJack()結構圖(Structure Chart) 計算各牌點數 無A時判斷 Points()
追求簡單清楚的程式 選用簡單有效的演算法 選用簡單易修改的程式架構 消除多餘程式
BlackJack_0_0_1.Program using System; using System.Diagnostics; namespace BlackJack_0_0_1 { class Program static void Main(string[] args) Debug.Assert( BlackJackTest.Scenario_1_OK()); }
BlackJack_0_0_1.BlackJackTest片段 public static bool Scenario_1_OK() { Deck deck = new Deck(); HumanPlayer player = new HumanPlayer(); ComputerPlayer computer = new ComputerPlayer(); player.SaveACard(deck.DealACard()); computer.SaveACard(deck.DealACard()); return (player.IsBlackJack()); }
BlackJack_0_0_1.Deck 片段(1/2) private Card card1; private Card card2; private Card card3; private int nCards = 0; public Deck() { card1 = new Card(Suit.SPADE, 1); card2 = new Card(Suit.HEART, 11); card3 = new Card(Suit.DIAMOND, 10); nCards = 3; }
BlackJack_0_0_1.Deck 片段(2/2) public Card DealACard() { if (nCards == 3) { nCards--; return card1; } if (nCards == 2) { return card2; if (nCards == 1) { return card3;
BlackJack_0_0_1.HumanPlayer 片段(1/3) private Card card1; private Card card2; private int nCards = 0; public void SaveACard(Card card) { if (nCards == 0){ card1 = card; } if (nCards == 1){ card2 = card; ++nCards;
BlackJack_0_0_1.HumanPlayer片段 (2/3) public bool IsBlackJack() { int point1 = Points( card1.rank ); int point2 = Points( card2.rank ); bool isBlackJack = (point1 + point2 == 21); if (!isBlackJack && point1 == 1) point1 = 11; isBlackJack = (point1 + point2 == 21); }
BlackJack_0_0_1.HumanPlayer片段 (3/3) if (!isBlackJack && point2 == 1) { point2 = 11; isBlackJack = (point1 + point2 == 21); } return isBlackJack; private int Points(int rank) int points = rank; if (rank > 10) points = 10; return points;
BlackJack_0_0_1.ComputerPlayer Card card1; public ComputerPlayer() { } public void SaveACard(Card card) { card1 = card; }
練習 產生方案及專案BlackJack_0_0_2 複製類別BlackJack_0_0_1.Deck至BlackJack_0_0_2,並修改為BlackJack_0_0_2.Deck,使能同時供測試場景1、2之用 複製類別BlackJack_0_0_1.BlackJackTest至BlackJack_0_0_2,並增加函式Scenario_2_OK()以測試場景2 複製修改其他類別,使能同時測試場景1、2