簡體   English   中英

封裝集合是一種好的(正確的)方法嗎?

[英]Is it a good (correct) way to encapsulate a collection?

class MyContainedClass {
};

class MyClass {
public:
  MyContainedClass * getElement() {
    // ...
    std::list<MyContainedClass>::iterator it = ... // retrieve somehow
    return &(*it);
  }
  // other methods
private:
  std::list<MyContainedClass> m_contained;
};

雖然msdn說std::list不應該在刪除或插入時執行元素的重定位,但它是一種返回指向列表元素的指針的好方法嗎?

PS:我知道我可以使用指針集合(並且必須delete析構函數中的元素),共享指針集合(我不喜歡)等。

我沒有看到使用封裝這個,但這可能只是我。 在任何情況下,返回引用而不是指針對我來說更有意義。

一般來說,如果你的“包含類”真正包含在你的“MyClass”中,那么MyClass不應該允許外人觸摸其私人內容。

因此,MyClass應該提供操作包含的類對象的方法,而不是返回指向它們的指針。 因此,例如,一個方法,如“增加第十個包含的對象的值”,而不是“這里是指向第十九個包含的對象的指針,根據您的意願使用它”。

這取決於...

這取決於您希望課程的封裝程度,以及您想要隱藏或顯示的內容。

我看到的代碼對我來說似乎沒問題。 你是對的,在另一個數據/迭代器的修改/刪除的情況下,std :: list的數據和迭代器不會失效。

現在,返回指針將隱藏您使用std :: list作為內部容器的事實,並且不會讓用戶導航其列表。 返回迭代器可以讓更多的自由為這個類的用戶導航這個列表,但是他們會“知道”他們正在訪問一個STL容器。

我想,這是你的選擇。

請注意,如果它== std :: list <>。end(),那么您將遇到此代碼的問題,但我想您已經知道了,並且這不是本討論的主題。

不過,還有其他我總結如下:

使用const將有助於......

你返回一個非const指針的事實讓你的對象靜默地修改他/她可以得到他/她的任何MyContainedClass,而不告訴你的對象。

相反或返回一個指針,你可以返回一個const指針(並用const后綴你的方法)來阻止用戶修改列表中的數據而不使用你認可的訪問者(一種setElement ?)。

  const MyContainedClass * getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return &(*it);
  }

這將在某種程度上增加封裝。

參考怎么樣?

如果你的方法不能失敗(即它總是返回一個有效的指針),那么你應該考慮返回引用而不是指針。 就像是:

  const MyContainedClass & getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return *it;
  }

這與封裝無關,但是:..- p

使用迭代器?

為什么不返回迭代器而不是指針呢? 如果對你來說,上下導航列表是可以的,那么迭代器將比指針更好,並且大多以相同的方式使用。

如果要避免用戶修改數據,請使迭代器成為const_iterator。

  std::list<MyContainedClass>::const_iterator getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return it;
  }

好的一面是用戶可以瀏覽列表。 壞的一面是用戶會知道它是一個std :: list,所以......

Scott Meyers在他的書“ Effective STL:50改進您對標准模板庫的使用的具體方法”中表示,不值得嘗試封裝容器,因為它們都不能完全替換為另一個容器。

好好的,你真正想要的MyClass進行。 我注意到一些程序員只是為了習慣而為他們的集合編寫包裝器,無論他們是否有超出標准STL集合所滿足的特定需求。 如果這是你的情況,那么請輸入typedef std::list<MyContainedClass> MyClass並完成它。

如果您確實要在MyClass實現操作,那么封裝的成功將更多地取決於您為它們提供的接口,而不是您如何提供對基礎列表的訪問。

沒有冒犯意味着,但是......由於你提供的信息有限,它聞起來像是你在試圖:暴露內部數據,因為你無法弄清楚如何在MyClass實現客戶端代碼所需的操作......或者可能,因為您甚至不知道客戶端代碼將需要哪些操作。 這是嘗試在需要它的高級代碼之前編寫低級代碼的典型問題; 你知道你將要使用哪些數據,但還沒有確切地確定你將要用它做什么,所以你編寫了一個將原始數據一直暴露到頂層的類結構。 你最好在這里重新考慮你的策略。


@cos

當然我封裝MyContainedClass不僅僅是為了封裝。 讓我們來看一個更具體的例子:

在你知道它們將被用於什么之前,你的例子幾乎沒有減輕我對你正在編寫容器的恐懼。 您的示例容器包裝器 - Document - 總共有三個方法: NewParagraph()DeleteParagraph()GetParagraph() ,所有這些方法都在包含的集合( std::list )上運行, 並且所有這些方法都密切反映了std::list提供“開箱即用”。 Document封裝了std :: list,因為客戶端不需要知道它在實現中的用途......但實際上,它只不過是一個外觀 - 因為你提供客戶端指向存儲在列表中的對象的原始指針,客戶端仍然隱含在實現中。

如果我們將對象(不是指針)放到容器中,它們將被自動銷毀(這很好)。

好壞取決於系統的需求。 這個實現意味着什么很簡單:文檔擁有Paragraph s,當從文檔中刪除Paragraph時,任何指向它的指針都會立即變為無效。 這意味着在實現以下內容時必須非常小心:

除了使用段落集合之外的其他對象,但不擁有它們。

現在你有問題了。 您的對象ParagraphSelectionDialog具有指向Document擁有的Paragraph對象的指針列表。 如果你不小心來協調這兩個對象,該Document -或者通過方式的另一個客戶端Document -可能無效的部分或全部通過實例舉行的指針ParagraphSelectionDialog 沒有簡單的方法可以捕獲這個 - 指向有效Paragraph的指針看起來與指向解除分配的Paragraph的指針相同,甚至可能最終指向一個有效但不同的Paragraph實例! 由於客戶端被允許,甚至預期會保留和取消引用這些指針,因此一旦從公共方法返回, Document就會失去對它們的控制權,即使它保留了Paragraph對象的所有權。

這是不好的。 你最終會得到一個不完整的,膚淺的,封裝的,漏洞的抽象,並且在某些方面它比沒有抽象更糟糕。 因為隱藏了實現,所以客戶端不知道接口所指向的對象的生命周期。 大多數時候你可能會很幸運,因為大多數std::list操作不會使對它們不修改的項的引用無效。 一切都會很好......直到錯誤的Paragraph被刪除,你發現自己仍然需要通過callstack跟蹤尋找保持指針有點太長的客戶端的任務。

修復很簡單:返回值或對象,只要需要就可以存儲,並在使用前進行驗證。 這可能是一個簡單的序數或ID值,必須傳遞給Document以換取可用的引用,或者像引用計數的智能指針或弱指針那樣復雜......它實際上取決於具體的需求你的客戶。 首先指定客戶端代碼,然后編寫您的Document以進行服務。

簡單的方法

@cos,對於你所展示的例子,我想說用C ++創建這個系統最簡單的方法就是不要引用引用計數。 您所要做的就是確保程序流首先銷毀對象(視圖),該對象在根文檔被銷毀之前保存對集合中對象(段落)的直接引用。

艱難的方式

但是,如果您仍希望通過引用跟蹤來控制生命周期,則可能必須將引用保持在層次結構的更深處,以便Paragraph對象保存對根Document對象的反向引用,這樣,只有當最后一個段落對象被銷毀時,才會對Document對象執行操作。被毀壞了。

此外,段落引用在Views類中使用時,當傳遞給其他類時,也必須作為引用計數接口傳遞。

韌性

與我在開頭列出的簡單方案相比,這是一個太多的開銷。 它避免了各種對象計數開銷,更重要的是,繼承您的程序的人不會陷入與您的系統交叉的引用依賴性線程陷阱中。

替代平台

在支持和推廣.NET或Java等編程風格的平台中,這種工具可能更容易執行。

你仍然要擔心記憶

即使使用這樣的平台,您仍然必須確保以適當的方式取消引用對象。 其他傑出的參考文獻可能會在眨眼間消耗掉你的記憶。 所以你看,引用計數並不是良好的編程實踐的靈丹妙葯,盡管它有助於避免大量的錯誤檢查和清理,這在應用時整個系統大大簡化了程序員的任務。

建議

也就是說,回到原來的問題,引起所有引用計數的疑慮 - 是否可以直接從集合中公開你的對象?

程序不能存在於程序的所有類/所有部分彼此真正相互依賴的情況下。 不,這是不可能的,因為程序是您的類/模塊如何交互的運行表現。 理想的設計只能最小化依賴關系,而不是完全刪除它們。

所以我的意見是, 是的,將您對集合中對象的引用暴露給需要使用它們的其他對象並不是一種不好的做法,只要您以理智的方式執行此操作

  1. 確保程序中只有少數幾個類/部分可以獲得此類引用,以確保最小的相互依賴性。

  2. 確保傳遞的引用/指針是接口而不是具體對象,以便在具體類之間避免相互依賴性。

  3. 確保引用不會進一步傳遞到程序中。

  4. 在清理滿足這些引用的實際對象之前,請確保程序邏輯負責銷毀依賴對象。

我認為更大的問題是你隱藏了集合的類型,所以即使你使用不移動元素的集合,你可能會在未來改變主意。 在外部,這是不可見的,所以我說這樣做不是一個好主意。

當你從列表中添加或刪除東西時,std :: list不會使任何迭代器,指針或引用無效(顯然除了刪除項目的任何一點),因此以這種方式使用列表不會破壞。

正如其他人所指出的那樣,您可能不希望分發直接訪問此類的私有位。 所以將功能更改為:

  const MyContainedClass * getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return &(*it);
  }

可能更好,或者如果你總是返回一個有效的MyContainedClass對象,那么你可以使用

    const MyContainedClass& getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return *it;
  }

避免調用代碼必須處理NULL指針。

對於未來的程序員而言,STL比您的自定義封裝更熟悉,因此如果可以,您應該避免這樣做。 有些邊緣情況你還沒有想到哪些會在應用程序的生命周期中出現,而STL則經過很好的審核和記錄。

此外,大多數容器支持一些類似的操作,如begin end push等。因此,如果更改容器,更改代碼中的容器類型應該是相當簡單的。 例如,矢量到deque或映射到hash_map等。

假設您仍然希望以更深層次的理由執行此操作,我會說正確的方法是實現列出實現的所有方法和迭代器類。 無需更改時,將調用轉發給成員列表調用。 修改並轉發或執行一些需要執行特殊操作的自定義操作(之所以您首先決定這一點)

如果STl類設計為繼承而更容易,但為了提高效率,決定不這樣做。 谷歌“繼承STL課程”以獲得更多關於此的想法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM