从子包中隐藏记录

Hiding record from child packages

package Parent is

   type Item is private;
   
   function Get return Item;
   
private
   
   type Item is
      record
         Value : Boolean;
      end record;
   
   procedure Set 
     (Object : Item;
      Value  : Boolean);

end Parent;

请告诉我在这个例子中如何防止直接从子包更改 Item 记录,从而保留调用私有方法 Set 的能力?

类型项目不是标记记录。因此它不能在子包中扩展。

你的问题很混乱。您显示的包声明没有声明可以从任何点“直接”更改的“项目记录”对象(变量);它只声明了一个名为 Item 的 record type 和一些子程序。子包在 运行 时所做的任何事情都不能改变记录类型;它在编译时是固定的。

也许你的例子不完整,没有反映出你真正的问题?

这是我对 Ada 的不满之一(只是极少数之一),它允许人们通过简单地为您的包制作子包来绕过隐私。我没有弄乱私有子包来查看我是否可以做一些工作,但是如果你对堆分配没问题的话,PIMPL 模式在 Ada 中确实有效。

基本上,您在包规范中创建了一个不完整的类型,并在私有记录声明中使用了该类型的访问参数。规范不知道记录不完整类型是什么样的,但由于您只使用对它的访问类型,规范将编译。还应该隐藏所有需要的私有操作,例如仅将 Set 隐藏到包体中。

然后在包体中完全定义不完整的类型,我建议使用 Ada.Finalization 以确保始终完全分配和释放参数。

我将给出一个完全可编译的示例(使用在线 tutorialspoint ada 编译器测试)。

我也不知道如何处理您的 Get 操作,所以只是将其默认为某些内容并添加了一个 Get_Value 操作来获取布尔值。你可以remove/adapt这个随你喜欢。

这不是最通用的解决方法,但它是我发现在 Ada 中有效的解决方法。同样,我还没有探索“私人”子包,看看它们是否可以以这种方式发挥作用,所以也许可以探索一些东西。

with Ada.Finalization;
with Ada.Unchecked_Deallocation;

with Ada.Text_IO; use Ada.Text_IO;
procedure Hello is
    
    package Parent is
    
        type Item is tagged private;
        
        function Get return Item;
        function Get_Value(Self : in Item) return Boolean;
       
    private
    
        type Private_Item;
        type Private_Access is access Private_Item;
       
        type Item is new Ada.Finalization.Controlled with record
            Impl : Private_Access := null;
        end record;
        
        overriding procedure Initialize(Self : in out Item);
        overriding procedure Finalize(Self : in out Item);
    
    end Parent;
    
    package body Parent is
    
        type Private_Item is record
            Value : Boolean := False;
        end record;
        
        procedure Set 
            (Object : in out Item;
             Value  : Boolean)
        is begin
            Object.Impl.Value := Value;
        end Set;
        
        -- What is this supposed to do????
        function Get return Item is (Ada.Finalization.Controlled with Impl => new Private_Item);
        
        function Get_Value(Self : in Item) return Boolean is
        begin
            return Self.Impl.value;  -- raises null exception if not initialized
        end Get_Value;
            
             
        procedure Initialize(Self : in out Item) is
        begin
            if Self.Impl = null then
                Self.Impl := new Private_Item;
            end if;
        end Initialize;
        
        procedure Free is new Ada.Unchecked_Deallocation(Private_Item, Private_Access);
        
        procedure Finalize(Self : in out Item) is
        begin
            if Self.Impl /= null then
                Free(Self.Impl);
            end if;
        end Finalize;
    
    end Parent;
    
    I : Parent.Item;

begin
    Put_Line("Hello, world!");
    Put_Line(Boolean'Image(I.Get_Value));
end Hello;

正如 Jere 所指出的,这是使用子 pkg 通过扩展提供编程的结果。通过扩展编程通常不是一个好主意,因为它强调易写性而不是易读性,并且违反了 S/W-engineering 原则。

Jere 介绍了使用访问类型从子 pkg 中隐藏实际类型的标准方法。这可行,但由于它涉及手动内存管理,因此很容易出错。

在不使用访问类型的情况下通过扩展编程避免此问题的一种方法是使用...更多扩展编程:

private -- Parent
   type Root is abstract tagged null record;

   function Equal (Left : in Root'Class; Right : in Root'Class) is
      (Left = Right);

   package Class_Holders is new Ada.Containers.Indefinite_Holders
      (Element_Type => Root'Class, "=" => Equal);

   type Item is record
      Value : Class_Holders.Holder;
   end record;
end Parent;

package body Parent is
   type Real_Item is new Root with record
      Value : Boolean;
   end record;

您可以将 Real_Item 存储在 Holder 中。检索值时,必须将其转换为 Real_Item:

R : Real_Item;
V : Item;
...
R.Value := True;
V.Value.Replace_Element (New_Item => R);
...
R := Real_Item (V.Value.Element);

有多种方法可以使用这种方法,其中 Root 可以是接口类型,而其他方法则不能。我总是使用抽象标记类型来避免记住哪个是哪个。

函数 Equal 是必需的,因为 class-wide 类型没有原始操作(请注意,GNAT 将在没有 Equal 且与 "=" 没有关联的情况下编译它,但这是一个编译器错误)。

是的,当然可以。嗯,有点。

但像大多数事情一样 Ada-esque,它需要一点思考和计划。

这是一种方法(唯一的方法?)

对应的声明是,

package GrandParent is
   type Item is private;
private
   type Item is
      record
         Value : Boolean;
      end record;
end GrandParent;

package GrandParent.Parent is
   function Get
     (The_Item : in Item)
      return Boolean;
end GrandParent.Parent;

private package GrandParent.Child1 is
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean);
end GrandParent.Child1;

包体是,

package body GrandParent.Child1 is
   
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean)
   is
   begin
      The_Item.Value := Value;
   end Set;
   
end GrandParent.Child1;

private with GrandParent.Child;

package body GrandParent.Parent is
   
   function Get
     (The_Item : in Item)
      return Boolean
   is
     (The_Item.Value);
   
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean)
   is
   begin
      GrandParent.Child.Set
        (The_Item => The_Item,
         Value    => Value);
   end Set;
   
end GrandParent.Parent;

如果你尝试拥有,

(private) with GrandParent.Child;
package GrandParent.Parent.Child is
end GrandParent.Parent.Child;

这会引发一个编译时错误,即 当前单元也必须是 GrandParent 的直接后代,从而有效地使 GrandParent.Child1 包私有至 GrandParent.Parent.

GrandParent 的客户也不会看到 GrandParent.Child1。但是,GrandParent 的其他 children 将具有与 GrandParent.Parent

相同的可见性

这就是隐藏 Set 子程序的方法。如果你想从包中隐藏私有类型怎么办children?

首先,这可能是有问题的,因为具有私有类型的包的 children 旨在与该类型完全交互,因为正如其他人所描述的那样,children 是关于扩展的功能他们各自的parent包。

如果你想这样做,那么最好的办法是将类型 Item 和 Get 和 Set 例程隐藏到 GrandParent.Child 中,这样只有 GrandParent.Parent 可以看到它们(在它的private body) 并公开任何你想让 GrandParent.Parent 的 children 在 GrandParent.Parent 包中拥有的功能。

但是,我不确定这是否特别有用。一个问题 - 如果 Parent 的 children 不应该访问 Item 的内部工作,为什么他们是 Parent 的孩子?