• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-5  評論-33  文章-0  trackbacks-0
            為了更好的討論這個問題,假設我們要設計一個關于people的class,并進行如下約定:
            1. age 小于 0 的baby不在我們的考慮范圍之內.
            2. age 大于 200 的people我們認為是不存在的.
            即,關于people的age,我們考慮的范圍是[0 , 200]
            基于以上假設,我們設計如下一個class,這里為了簡化代碼,突出問題的主體,將部分成員函數及成員變量去掉.

            class People
            {
                
            public:
                    
            explicit People( int iAge )
                    : m_iAge( iAge )
                    
            {
                    
                    }

            }
            ;

            看起來很簡單,下一步我們可以使用這個class了.

            People janice( 28 );
            janice.Sing();

            如果類似于上面這樣以合法的參數構造一個People,那么后續的操作也在我們的可控范圍之內,也是理想的情況.
            但是如果我們以非法的參數構造People并進行相關的函數調用,如下:

            People hebe( -1 );
            hebe.Sing();

            People ella( 
            201 );
            ella.Dance();


            那么這樣實在是太糟糕了,因為在調用Sing()和Dancing()時我們甚至不知道調用的對象的內部狀態是非法的,這也就成為bug的一個源泉.

            總結一下上面的例子中遇到的問題:在以特定的參數構造一個對象時,如果參數非法或構造失敗,我們應當向調用者反饋這一信息.

            對于一般的成員函數,如果能夠有返回值,那么我們可以通過返回值來標識傳遞給函數的參數非法或內部運行失敗這種情況.
            但是對于構造函數,因為它不能有返回值,所以,我們必須使用其它的方法來向調用者反饋"傳遞給構造函數的參數非法或構造失敗".

            針對于這一問題有三種解決方案:

            第一種方案:在構造函數的參數中傳遞一個額外的參數用于標識構造是否成功.
            在這種方案的指導下,代碼如下:

            class People
            {
                
            public:
                    People( 
            int iAge , bool &bInitStatus )
                    : m_iAge( iAge )
                    
            {
                        
            if( ( iAge < 0 ) || ( iAge > 200 ) )
                        
            {
                            bInitStatus 
            = false;
                        }

                        
            else
                        
            {
                            bInitStatus 
            = true;
                        }

                    }

            }
            ;


            然后我們可以這樣使用:

            bool bInitStatus = false;
            People hebe( 
            -1 , bInitStatus );
            iffalse == bInitStatus )
            {
                
            // handle error
            }

            else
            {
                hebe.Sing();
            }


            這種方法是可行的,但是代碼看起來過于丑陋且不夠直觀,這里只是作為一種方案提出并不推薦使用.

            第二種方案:使用兩段構造的形式.
            所謂的兩段構造是指一個對象的內部狀態的初始化分兩步完成,將構造函數中的部分初始化操作轉移到一個輔助初始化的成員函數中:
            第一步是通過構造函數來完成部分內部狀態的初始化.
            第二步是通過類似于 Initialize 之類的成員函數來完成對象內部狀態的最終初始化.

            兩段構造的形式在 MFC 中廣泛使用.在MFC中我們經常看到類似于 Initialize , Create 之類的函數.
            基于兩段構造的形式,代碼如下:

            class People
            {
                
            public:
                    People( 
            void )
                    : m_iAge( INVALID_AGE )
                    
            {
                    
                    }

                    
                    
            bool Initialize( int iAge )
                    
            {
                        
            if( ( iAge < 0 ) || ( iAge > 200 ) )
                        
            {
                            
            return false;
                        }

                        
            else
                        
            {
                            m_iAge 
            = iAge;
                            
            return true;
                        }
                        
                    }

            }
            ;


            在這種情況下,我們應當這樣來使用People:

            People hebe;
            const bool bStatus = hebe.Initialize( 20 );
            iffalse == bInitStatus )
            {
                
            // handle error
            }

            else
            {
                hebe.Sing();
            }


            這種方案似乎比第一種方案更優,但是仍有一個潛在的問題:對象是以兩步構造完成的.
            第一步構造是由構造函數來完的,OK,這一點我們不用擔心,編譯器幫我們保證.
            但是第二步是由類似于 Initialize 之類的成員函數來完成的,如果我們在構造一個People對象之后忘記了調用 Initialize ,
            那么這個對象的內部狀態仍然是非法的,后續的操作也將由此引發bug.這也是"兩段構造"這種形式受到詬病的原因之一.
            另一方面,"兩段構造"的形式與C++的"RAII",Resource Acquisition Is Initialization(資源獲取即初始化),這一原則相違背.
            因為以"兩段構造"這種形式設計的class People 在構造一個對象時,它的內部狀態實際上并沒有完全初始化,我們需要調用 Initialize 來輔助完成最終的初始化.
            所以,盡管"兩段構造"這種方案可以解決我們所遇到的"對構造函數參數非法進行反饋"這個問題,但是這種方案并不夠優雅.

            但是為什么MFC會先擇"兩段構造"這種形式呢,因為在C++發展的初期,當異常機制還不是足夠成熟,沒有得到廣泛的認可和使用時,
            MFC中選擇兩段構造或許也是情理之中的,也許還有其它的原因,類似的類庫還有ACE...

            當然,在某些情況下,使用兩段構造也有其獨到的好處.
            下面所設計的場景可能有一些牽強,但只是為了力求簡單并能夠說明問題.(代碼進行了大量簡化)

            class Server
            {
                
            public:
                    Server(
            void)
                    
            {
                        
            // allocate a huge chunk of memory to store data
                    }

                    
                    
            ~Server( void )
                    
            {
                        
            // free all the used resource
                    }

            }
            ;


            然后在我們的系統中,我們需要使用一個 server pool , 在系統啟動時,我們需要 server pool 中有 100 個 Server 可用.

            Server serverPool[ 100 ];


            在系統負載最大的時候,假定100個Server可以勝任,但是在大多數情況下,我們只需要少量的Server即可以完成任務.
            在這種情況下: Server serverPool[ 100 ]; 將會消耗大量的資源(而且大部分資源我們并不會使用),這是我們不愿意接受的.
            之所以出現這種情況,因為我們在構造函數中分配了大量資源,這種分配是隨構造函數的調用而自動完成的.


            這時,如果我們使用"兩段構造"的方法就能在一定的程度上解決這個問題.

            class Server
            {
                
            public:
                    Server(
            void)
                    
            {
                        
            // do nothing here.
                    }

                    
                    
            bool Initialize( void )
                    
            {
                        
            // allocate a huge chunk of memory to store data
                    }

                    
                    
            ~Server( void )
                    
            {
                        
            // free all the used resource
                    }

            }
            ;


            在這種情況下: Server serverPool[ 100 ]; 的開銷就很小了,我們可以很好地控制對系統資源的使用,而不會浪費.
            當然,當我們從 serverPool 中獲取一個 Server 對象時,我們要調用 Initialize 進行最終的初始化操作.


            第三種方案:使用異常
            即是當用于構造 People 對象的參數非法時,我們選擇在構造函數中拋出一個異常來反饋給調用者"參數非法,構造失敗"的相關信息.

            class People
            {
                
            public:
                    
            explicit People( int iAge ) throw( std::invalid_arguement )
                    : m_iAge( iAge )
                    
            {
                        
            if( ( iAge < 0 ) || ( iAge > 200 ) )
                        
            {
                            
            throw std::invalid_arguement( "invalid argument" );
                        }

                    }

            }
            ;


            那么我們可以這樣使用:

            try
            {
                People hebe( 
            20 );
                hebe.Sing();
            }

            catchconst std::invalid_arguement &refExcept )
            {
                
            // handle exception here.
            }


            這種方案似乎是最優的:符合RAII原則,也符合B.S等一批老大推行的"現代C++程序設計風格".

            但是很多在開發一線上的同學們都反對在代碼中使用異常,實際上我也不愿意在代碼中使用異常,
            至少不愿意看到類似于java代碼中那樣鋪天蓋地的"throw try catch".
            我對異常的使用也僅僅是局限在類似于那些適合"用異常來代替兩段構造"的場景中,對于其它的情況,
            我更愿意用返回錯誤碼來標識函數內部的運行狀態,而不是通過拋出異常的形式來告知調用者.


            C++規定:如果執行構造函數的過程中產生異常,那么這個未被成功構造的對象的析構函數將不會被調用.
            這一點在很大程度上為我們在構造函數中拋出異常的安全性提供了C++語言級的保證,當然,其它的安全性需要我們自己保證.

            對于"向調用者反饋構造函數參數非法或構造失敗的相關信息"這個問題,"基于異常"和"基于兩段構造"這兩種方案我都使用過一段時間,
            目的是確定對于自己而言到底哪一種方案用起來更舒服更適合自己.最終的結果是我選擇了"基于異常"這種形式.

            對于"基于異常"和"基于兩段構造",沒有哪一種能在所有的情況下都是最優的解決方案,印證了那句名言"there is no silver bullet".
            如何在不同的場景中選擇其一作為最優的解決方案是我們在設計時需要權衡的問題.


            個人愚見,錯漏之處還請指正,歡迎大家踴躍發言:)

            posted on 2010-03-04 21:23 luckycat 閱讀(2755) 評論(18)  編輯 收藏 引用 所屬分類: C++

            評論:
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-04 22:03 | Corner Zhang
            呵呵!如果你要傳統的編程風格,就用Initialize() Shutdown()的“兩段構造”;若要趨向于捕捉異常的try catch的風格,就用構造器上的異常處理唄!

            具體取舍,看項目人員本身素質,按我的經驗,傳統風格的代碼易于調試跟蹤,錯誤就在附近。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-04 22:22 | qiaojie
            兩種方法都是錯誤的用法,這地方應該使用斷言而不是異常或者錯誤返回值。傳入錯誤的參數值是程序的BUG而不是真正的異常,異常是由于外部運行環境的錯誤引起而非程序本身的BUG(例如,內存耗盡,網絡錯誤,文件錯誤等等),異常處理只應該用在真正屬于異常的情況。當然,有的時候People的age參數來自于用戶的輸入,這個時候也不應該使用異常,而是在用戶輸入完成,對話框結束,構造People之前,加入一個ValidateUserInput()函數來校驗用戶輸入,如果age屬于非法值,彈出一個錯誤對話框向用戶說明錯誤的原因。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-04 22:32 | luckycat
            @Corner Zhang
            關于這一點,我們的觀點還是很相近的,不同的編程風格決定了不同的設計取舍以及代碼風格,
            沒有哪一種一直都是最優的,選擇一種適合自己的并一直堅持下去(當然,適當的時候還是要變通一下).
            實際上我并不愿意在代碼中大量使用"throw try catch",所以,基本上我不愿意去看java代碼,
            就如你所說的,傳統的代碼風格易于跟蹤調試;在我看來,傳統的"基于錯誤碼"的代碼比基于"使用異常"的代碼
            要緊湊得多,因為我們可以在錯誤發生的地方立即處理錯誤,而不像"基于異常"的代碼中我們要向下跨越N行代碼
            來進行錯誤處理(這一點使得代碼的可讀性很差).
            而且,如果try代碼塊中太大,那么在對應的catch塊中盡管我們可以進行相應的異常處理,但是此時我們卻失去了
            對發生錯誤的代碼上下文的必要了解.這一點使得我們降低了對代碼整體運行流程的可預知性,
            更重要的是也降低了錯誤處理的針對性,因為同一種類型的異常可能由try代碼塊中的多個地方throw.具體是哪一個throw的無從了解.
            so,我的觀點是:讓構造函數盡量簡單,減少誤用的可能性,并增加構造函數的安全性(盡量減少構造函數構造失敗的可能性).
            這樣我們也就能在一定程度上減少對異常機制的依賴.至于其它的可帶有返回值的成員函數都使用"返回錯誤碼"來取代"拋出異常".
              回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-04 23:25 | luckycat
            @qiaojie
            呵呵,可不能一棒子打死啊.
            至于說,類似于"用戶輸入非法數據"之類的問題到底是算作錯誤還是異常情況,這一點依賴每個人對同一個事物的認知,
            有的人認為這是異常情況,有的人認為這是錯誤,這個認知層面上的問題我們先不討論,尊重每個人的看法.
            實際上,即使存在上面的認知差異也沒有關系,因為問題的本質是對"用戶輸入非法數據"這種異常也好,錯誤也好,
            我們在代碼邏輯中應該如何處理,你覺得應該用類似于"對話框+ValidateUserInput"之類的方法來處理,
            我覺得可以通過返回錯誤碼或拋出異常的形式來做處理. 本質上都是在處理一種"非正常情況",只是我們的處理方式不同,
            你說應該用你的方法比較好,我覺得用我的方法處理也是可行的,到底用哪一種呢,
            即使在這種情況下,我們還是很難選擇一個所有人都接受的處理方式. 這里就涉及到設計的權衡和取舍了,有很多種方法都可行,我們尊重每個人在特定的環境中所做出的選擇.

            /*
            "而是在用戶輸入完成,對話框結束,構造People之前,加入一個ValidateUserInput()函數來校驗用戶輸入,
            如果age屬于非法值,彈出一個錯誤對話框向用戶說明錯誤的原因"
            */
            你這里只是對一種特例的處理,實際上我們很難在所有的情況都保證傳入構造函數的參數是合法的,要是我們真的找到了這樣一種方法,
            那么"there is a silver bullet !" , 接下來,對于所有奮斗在開發一線的同學們而言,生活就要美好很多,應該再也不會發生類似于"小貝"的悲劇了:)

            在你處理的特例中,既然我們能夠保證傳入構造函數的參數一定是合法的,那確實太好了,"使用異常"和"兩段構造"都是多余的.
            對于那種我們不能確保傳入構造函數的參數是一定是合法的情況,我們該選擇哪種處理方式呢,這是這篇文章討論的根本問題.
            如果因為構造函數的參數不合法,或者因為其它的原因構造失敗,最基本的一點,我們應當讓調用者知道這一情況,
            至于調用者如何處理就不在我們關心的范圍之內了,是"彈出對話框告知用戶重試","忽略這個錯誤",還是直接"abort",不同的場景下也有不用的選擇.
            我們要做的就是在"構造一個對象發生異常時"告知調用者"發生了非正常情況".

            這篇文章的主題也就是討論"在構造發生非正常情況時采取何種方式來告知調用者這一情況".
            對于這個問題很難有一個"放之四海而皆準"的處理方案,因為這涉及到不同的編程風格,應用場景和設計時的取舍.

            不過我們還是可以踴躍地發表自己的看法,在討論和交流的過程中我們總能發現思維的閃光點.互相學習:)  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 08:34 | 飯中淹
            我一般會用一種類似工廠的方法。

            class CPeople
            {

            int m_iAge;

            CPeople() : m_iAge(-1) {}
            ~CPeople() {}
            bool _Init( int iAge )
            {
            if( iAge <= 0 || iAge > 200 )return false;
            m_iAge = iAge;
            return true;
            }
            public:
            static CPeople * Create( int iAge )
            {
            CPeople * people = new CPeople();
            if( people != NULL &&
            !people->_Init(iAge) )
            {
            people->Release();
            people = NULL;
            }
            return people;
            }
            void Release() { delete this;}
            };


            私有的構造函數和西狗函數確保他們不能被單獨分配和刪除
            所有的途徑只有create和release。

              回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 09:04 | LOGOS
            @飯中淹
            agree
            這正是我想說的。另外,在一些情形下構造函數不易調試,而兩段構造則能避開這一調試,選擇更好的調試的init  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 10:31 | 肥仔
            @飯中淹
            每個對象都要顯示的create和release。那基本上等價于放棄了C++構造函數和析構函數這兩個特性。
            對于棧對象,這樣會很累,也很容易泄漏。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 12:02 | luckycat
            @飯中淹
            將"兩段構造"與"Fatcory Pattern"結合起來確實是一種巧妙的設計!
            內部實現上還是"兩段構造",但是對于 class 的用戶而言,class CPeople 展現的卻是一個單一的"構造接口",
            用戶一旦調用這個接口"構造對象",那么"兩段構造"自動完成,極大地減少了"兩段構造"中因為忘記調用"Initialize"所帶來的問題.
            class CPeople 中的 Create 和 Release 所扮演的角色類似于"構造函數和析構函數",都是進行資源的分配與回收操作.
            單純從"資源管理"的角度來說,肯定是"構造函數和析構函數"相比如"Create 和 Release"更優一些,
            因為"構造函數和析構函數"對于"非動態分配的對象以及非placement new方式生成的對象",
            構造和析構都會由編譯器保證正確自動地調用,大大簡化了對資源的管理,或許這也是C++設計構造和析構的出發點之一.

            在"兩段構造" & "Fatcory Pattern"這種模式下,所有的CPeople對象將都由 Create 接口創建,這勢必需要我們管理大量的動態分配的對象,
            在這種情況下,如果稍有不慎,我們將面臨"resource leak"的問題.這個時候如果我們能將動態分配的CPeople對象用一種更方便安全的方式來管理就更好了,
            于是我想到了boost::shared_ptr,不知道大家想到了什么?
            類似于下面這樣:

            void FreeResource( CPeople *pPeople )
            {
            if( NULL != pPeople )
            {
            pPeople -> Release();
            }
            }

            CPeople *pHebe = CPeople::Create( 2 );
            if( NULL == pHebe )
            {
            // handle error
            }

            boost::shared_ptr< CPeople > pPeople( pHebe , FreeResource );

            下面我們就可以使用 pPeople 這個智能指針"do whatever you want" :) ,而且使用起來直觀方便:
            pPeople -> Sing();
            也減少了對動態分配資源進行管理的復雜度.



              回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 17:36 | 望見
            為了程序的健壯性,多余的操作也是必須的。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 18:00 | qiaojie
            @luckycat
            你的根本問題在于你沒理解異常的使用哲學,在錯誤的前提下去談什么設計的選擇根本就是一件毫無意義的事情。你如果要堅持自己的錯誤認識,去談什么尊重每個人的選擇,那我只能自認是對牛彈琴了。

            再來看為什么我說要用ValidateUserInput而不是其他什么方法,因為驗證用戶的輸入通常來說都是一個復雜的且經常可變的需求,我們假設Person的構造參數里有一個UserName,通常UserName的驗證會比較復雜,有長度限制,非法字符限制,重名限制等等,對于重名限制往往還要去用戶管理器或者后臺數據庫查詢一下,現在來看看把這些驗證邏輯都寫到Person的構造函數里是多么傻X的做法啊,首先,Person的構造函數需要依賴用戶管理器或者后臺數據庫才能驗證UserName的合法性。其次,當你的構造函數返回了錯誤值或者異常的時候,外部的處理代碼卻根本不知道為什么這個用戶名是非法的,所以要么還得再寫一遍驗證邏輯來檢查哪里非法,要么直接告訴用戶輸入非法,讓用戶摸不著頭腦。
              回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 19:33 | luckycat
            @qiaojie
            也許你的異常哲學是正確的并值得大家學習,還請你發文一篇讓大家有一個學習的機會,
            如果從你的文章中我確實發現了自己的錯誤,也會從中有所改正,當然,你也不需要"對牛彈琴"這個詞語.
            我這篇文章中的"People"只是一個用于作為討論基礎的例子,根本的問題是對于"構造函數的參數非法或是構造失敗"時,我們應當如果告知調用者.
            我并沒有說一定要把參數的合法性全部放在構造函數中完成,但是在構造函數中檢查參數的合法性是應該的,
            就像上面的同學說的"為了程序的健壯性,多余的操作也是必須的"。
            在這個例子中,你可以往自己熟悉的GUI方向進行特化,所以你可以使用"對話框"之類的工具來進行傳入構造函數之前的參數合法性檢驗以及進行相關的錯誤處理,
            但是在那些"非GUI"的領域,在那些"我們不能確保傳入構造函數的參數一定是合法的,不能保證構造函數一定會構造成功"的情況下,我們到底該如何處理,
            我考慮到可以使用"基于異常"或"基于兩段構造的形式".
            C++提供的異常機制是一種工具,可以作為"函數內部向函數的調用者傳遞函數內部非正常運行狀態"的一種方法.
            就如同你說的"內存耗盡,網絡錯誤,文件錯誤"這種情況下是異常,也許這種情況下我們應當使用"異常機制"(希望沒有理解錯).
            但是如果一個函數內部可能出現"內存耗盡"也會出現"參數非法的問題"(再重申一遍,我們不能永遠都保證傳入每一個函數的參數都是合法的).
            "內存耗盡"這種情況我們使用異常,但是"參數非法問題"我們使用什么呢,
            按照你的看法,"參數非法"不屬于異常的范圍之內,我們不應該使用"異常的形式",但我們還是要告知用戶"參數非法"的信息,
            假定這里我們"無法使用類似于彈出對話框的形式來告知用戶參數非法",那么我可以想到的告知調用者這一信息的方式是"使用錯誤碼",
            當然,我們還可以選擇"errno"的形式.
            這樣一來,我們就面臨一個問題"一個函數會以異常和錯誤碼兩種方式來告知調用者相關的非正常運行信息",
            接下來,調用者就要同時使用"try catch"和檢查函數的錯誤碼兩種方式來檢查函數的運行狀態,
            我覺得如果真的這樣設計函數的話,這就是一種很糟糕的設計,不知道你怎么認為.

            在告知調用者一個函數內部的"非正常狀態"時,我只會擇優使用"錯誤碼"或"異常這兩種形式"之一,不會同時使用.
            基于這一點,如果我選擇"以錯誤碼的形式"來反饋給調用者,那么在函數內部"網絡錯誤"時我也會使用錯誤碼來告知調用者(按你的看法,這種情況應該使用異常),
            如果我選擇"基于異常"的形式,那對"參數非法"的信息我也會拋出"std::invalid_arguement".這是設計上的取舍產生的必然選擇.


            說到這里,不知道你對于作為std異常類型之一的"std::invalid_arguement"這個詞語有什么感想,
            我覺得你應該向標準委員會指明"std::invalid_arguement"這個詞語,
            "從使用異常的哲學上的角度上來看這個概念是錯誤的,因為參數非法根本就不是異常,我們又怎么能因為參數的非法而throw std::invalid_arguement,
            這是在誤導廣大的std用戶,所以必須去掉".  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 21:56 | qiaojie
            @luckycat
            保證參數的正確性是調用者的責任,而不是被調用的函數的責任,如果你對此有疑問的話可以去閱讀一下契約式編程。當然,我并沒有說不需要去檢查參數的合法性,而是強調應該用assert(0 < age && age < 200);這樣的斷言來檢查參數合法性,這個為什么強調不要用異常或者錯誤返回值,是因為盲目的大量使用這類錯誤處理機制會導致整個項目變得混亂,如果任意一個帶參數的函數都會因為參數非法而拋異常,那么我在外面接到異常的時候會非常困惑,到底該如何正確處理那么多可能出現的異常?如果是用錯誤返回值的話就更麻煩,每個函數調用都要進行檢查,代碼及其繁瑣。錯誤處理機制是整個軟件設計的重要環節,他不像接口設計那么顯而易見,所以在設計上更應該小心規劃合理使用,在
            哪里會出異常,哪里該接異常應該做到心中有數正確處理,否則就會陷入混亂。
            當然,凡事無絕對,我說的這種使用方式并不適用于組件級或者系統級程序,系統級程序必須要采用防御性編程策略來檢測參數的合法性并向調用者報告錯誤(因為無法預期調用者如何調用函數),這常需要付出代價,常導致系統級提供的API接口跟應用程序之間阻抗適配,需要在應用程序內進行適當的封裝。而你的例子顯然不屬于這類程序。

            另外你拿std::invalid_arguement出來我覺得毫無意義,C++標準里不成熟不完善甚至很爛的東西多了去,說上三天三夜也說不完。C++的異常機制尤其不完善,備受爭議。像std::invalid_arguement基本沒人會去用。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-05 22:40 | luckycat
            @qiaojie
            謝謝賜教!

            "像std::invalid_arguement基本沒人會去用"這句話說得有點絕對了,用的人應該還是有一些的,可能我們沒有接觸到.
            另外,我們總是被"天朝"代表,想不到這次被 qiaojie 代表了:)

            你說"保證參數的正確性是調用者的責任,而不是被調用的函數的責任",
            這一點我也同意,不過我覺得作為函數的設計者,我們不應當對用戶所傳遞的參數有太多理想化的假設,所以我們應當在函數中進行參數合法性的檢查,
            一方面,可以在函數的入口處盡早發現非法參數的問題,這樣就不至于后續會使用錯誤的參數在函數中進行一些無意義的操作.
            另一方面,在函數入口處檢查參數的合法性,可以增強函數的健壯性,進一步增強系統的健壯性.
            舉個例子,如果傳遞給函數的實參不應該是NULL指針,用戶卻以NULL作為實參調用函數,假設我們沒有進行對應參數合法性檢查,
            那么后續基于這個NULL實參的操作可能會導致系統"coredump".

            對于參數的合法性檢查,在debug版本和release版本下應該都需要進行,類似于"assert(0 < age && age < 200);"這種檢測參數的合法性的代碼只在debug版本下可以起作用,
            在release版本下就不起用了,也就不能在release版本下作為參數合法性檢查的工具.
            在debug版本下,如果assert斷言失敗,那么我們可以看到對應的abort信息,然后程序異常退出.
            實際上這樣做可能有的時候并不合適,因為在一些情況下,僅僅是參數非法,我們可以進行相應的處理而不需要系統因此而退出運行.

            "強調不要用異常或者錯誤返回值,是因為盲目的大量使用這類錯誤處理機制會導致整個項目變得混亂"
            這句話如果僅僅是理論上來探討"如何讓系統設計的更優雅",那么這無疑可以作為一個"系統設計準則",
            但是在實際的開發過程中,有的時候一個函數內部出現"非正常情況"的可能性實在是太多了,我們必須要進行相應的處理.
            如果我們既不使用"異常"也不使用"返回錯誤碼"的形式來告知調用者,
            那么在反饋給調用者"函數內部出現非正常情況"這一點上我們將"無能為力",但我們又必須在這一點有所作為.

            在大多數情況下,"異常"和"錯誤碼"可能是我們僅有的兩個選擇方案,如何選擇其一作為最終的處理方案,
            甚至如何在不使用"異常"和"錯誤碼"的前提下也達到相同的效果,這是一件很"糾結"的事情.


            追求系統在架構和代碼設計上的完美是開發者的一個方向,但是有時我們需要考慮"追求完美的代價",
            在時間,人力以及成本的多重影響下,很多時候我們必須放棄對最優方案的探索,而選擇一種"不那么完美但是可行,可以很好解決問題"的方案.
            也許這個時候作為函數調用狀態反饋的"異常"和"錯誤碼"機制會在我們的思考和運用范圍之內.


              回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-06 08:55 | chentan
            qiaojie 討論很精彩
            我的習慣是 模塊邊界會檢查參數合法性,并報告給調用者
            模塊內部的代碼盡量多的用assert  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-06 21:06 | 陳梓瀚(vczh)
            對于類庫的設計者,其實有一個很簡單的判斷標準。

            1:如果一個輸入錯誤,調用者必須知道并處理,那么就采用“不處理就崩潰”方法,迫使調用者處理。在C++里面的唯一方法就是使用異常了。舉個例子,對一個容器的下標訪問越界,這個時候operator[]不可能返回一個可用的返回值,而且也不能用錯誤信息去污染返回值,譬如返回pair<bool, T>,因此直接拋出異常。

            2:如果一個構造函數發生錯誤之后,這個對象是絕對不能被碰的,那么采用異常。因為在try-catch結構里面,構造函數完蛋了,那么那個正在被構造的對象你是沒有辦法獲取的。

            3:異常可以用來在大量函數互相遞歸的時候(譬如說語法分析器)迅速跳到最外層,此處作為控制代碼流程而使用。

            這里我也接受一個例外,譬如說2,如果構造函數發生錯誤但是我并不想立刻拋出異常(因為有些狀態在構造函數里面想知道也比較麻煩),那么除了需要一個類似bool IsAvailable()const函數來告訴你以外,所有該類成員的非法操作(譬如說在錯誤的構造狀態下調用一個成員函數)都必須拋出異常,或者【絕對不產生任何副作用】的同時給一個返回值說明調用失敗。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-06 21:27 | luckycat
            @陳梓瀚(vczh)
            你列舉的判斷標準都值得借鑒,不過你后續補充的對"例外情況"的處理方式不敢茍同:
            因為構造可能沒有成功,那么我們需要調用IsAvailable之類的函數,甚至于后續需要因為判斷之前構造函數的狀態來
            對調用的每個成員函數進行"try catch"或者還要從"每個成員函數的返回值中來判斷之前的構造操作是否成功".
            這種設計是可行的,但對類的使用者來說太復雜.
            這種情況下我覺得使用"兩段構造"可能更好一些,我們只需要判斷"兩段構造"是否成功即可,如果構造成功,在后續的成員函數調用過程中,
            就再也不用為了確認構造函數的狀態來對每個被調用的成員函數進行"try catch"或檢查返回值的操作,這樣的設計應該更簡潔一些.  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2010-03-08 01:28 | 陳梓瀚(vczh)
            @luckycat
            兩段構造的話,你的類的其他成員函數也應該在構造不成功的時候拋出異常,并沒有任何區別,我的例外還減少了別人可能忘記第二段構造的概率。

            當然這是對一個庫的要求,應用程序不一定要如此嚴格。  回復  更多評論
              
            # re: 設計的兩難:選擇異常還是兩段構造 2015-01-27 23:09 | tsgsz
            拋異常不析構可以令所有的成員變量都是 std::unique_ptr  回復  更多評論
              
            无码任你躁久久久久久久| 狠狠色丁香婷综合久久| 精品久久久久久久久免费影院| 色播久久人人爽人人爽人人片AV| 久久综合亚洲色HEZYO社区| 久久青青草原亚洲av无码app| 日本精品久久久久中文字幕8| 久久天天躁夜夜躁狠狠| 香蕉久久一区二区不卡无毒影院| 精品久久人人爽天天玩人人妻| 国产精品久久久天天影视| 2020国产成人久久精品| 成人国内精品久久久久影院VR | 精品99久久aaa一级毛片| 精品久久久中文字幕人妻| 久久久久99精品成人片| 久久精品国产福利国产秒| 久久丫精品国产亚洲av不卡 | 久久久久免费精品国产| 久久精品亚洲男人的天堂| 97r久久精品国产99国产精| 久久香综合精品久久伊人| 无码国内精品久久人妻麻豆按摩| 伊人久久免费视频| 久久―日本道色综合久久| 精品国际久久久久999波多野 | 天天爽天天爽天天片a久久网| 久久亚洲欧美国产精品| 久久狠狠爱亚洲综合影院| 久久久久亚洲AV无码观看| 中文字幕无码av激情不卡久久| 久久婷婷色综合一区二区| 久久精品国产一区二区三区 | 人人狠狠综合久久亚洲婷婷| 久久被窝电影亚洲爽爽爽| 久久久久久狠狠丁香| 久久国产一区二区| 国内精品免费久久影院| 久久国产成人午夜aⅴ影院 | 无码任你躁久久久久久老妇| 99久久综合国产精品免费|