跳转到内容

Ada 编程/库/Ada.Streams

来自 Wikibooks,开放世界中的开放书籍

Ada. Time-tested, safe and secure.
Ada. 久经考验,安全可靠。

此语言特性从 Ada 95 开始提供。

Ada.Streams预定义语言环境 自 Ada 95 以来的一部分。

Ada **流** 是一种强大的 I/O 机制,允许将任何类型的对象读写到任何类型的“*介质*”(例如,网络连接、磁盘上的文件、磁带、内存缓冲区)。对于初学者来说,流可能有点难以理解,这是因为“双重通用性”:关于要写入/读取的对象的通用性,以及关于所涉及介质的通用性。本节的目的是对 Ada 流进行直观的介绍,省略一些细节。有关 Ada 流更精确和详细的描述,请参阅 Ada 参考手册,特别是 13.13: 流 [带注释的]

语言设计者将通过介质写入对象的难题分解为两个子问题

  1. 将对象转换为比特序列
  2. 通过流写入比特

请注意,第一步仅取决于要发送的对象,而不取决于实际介质。另一方面,第二步的细节仅取决于所使用的介质,而不取决于对象类型。

类似地,为了从网络连接“读取”对象,必须

  1. 从流中读取比特块
  2. 解析读取的块并将其转换为对象

再次注意,第一步仅取决于介质,而第二步仅取决于对象类型。


抽象流

[编辑 | 编辑源代码]

Ada 流的抽象模型基本上是一系列 *原始数据* (Stream_Element),可以以块的形式读写。这种抽象视图在 Stream 包定义中被形式化(来自 RM 13.13.1: 包 Streams. [带注释的] 添加了一些省略和注释)

  package Ada.Streams is
  
     type Root_Stream_Type is abstract tagged limited private;
  
     -- Elementary piece of data.  A Stream is a sequence of Stream_Element
     type Stream_Element is mod implementation defined;
   
     type Stream_Element_Offset is range implementation defined;
   
     -- A block of data
     type Stream_Element_Array is 
         array (Stream_Element_Offset range <>) of aliased Stream_Element;
    
     -- Abstract procedure that reads a block of data
     procedure Read (
          Stream : in out Root_Stream_Type;
          Item   : out Stream_Element_Array;
          Last   : out Stream_Element_Offset) is abstract;
  
     -- Abstract procedure that writes a block of data
     procedure Write (
          Stream : in out Root_Stream_Type;
          Item   : in Stream_Element_Array) is abstract;
  
  private
         implementation defined...
  end Ada.Streams;

由于类型 Root_Stream_Type 是抽象,无法创建 Root_Stream_Type 类型的对象,而必须首先从 Root_Stream_Type 派生一个新类型。Ada.Streams 仅指定了流必须提供的 *最小* 接口:使用流,我们必须能够

  • **读取** 一块数据(使用过程 Read)以及
  • **写入** 一块数据(使用 Write)。

通常,对于每种新的介质(例如,网络连接、磁盘文件、内存缓冲区),都将派生一个专门用于读写该介质的新类型。请注意,Read 和 Write 都是抽象的,因此任何非-抽象 类型必须必然用处理从特定介质读写细节的新过程覆盖它们。

请注意,Ada.Streams 的最小接口不包括例如用于打开或关闭流的函数,也不包括用于检查例如 End-Of-Stream 条件的函数。这是合理的,因为这些函数接口的细节取决于特定介质:用于打开与文件关联的流的函数将期望文件名作为参数,用于打开网络流的函数可能期望网络地址,而用于打开与内存缓冲区关联的流的函数可能需要缓冲区的地址和大小。派生自 Root_Stream_Type 的包将负责定义这些“辅助”函数。

序列化函数

[编辑 | 编辑源代码]

Ada 流系统中的第二个成分是我们称为 *序列化函数* 的东西,即负责将 Ada 对象转换为 Stream_Element 序列以及反之亦然的函数。实际上,我们将在下一刻看到,序列化函数不会通过来回传递 Stream_Element 数组来与调用者交互,而是直接与流交互。

与给定类型关联的序列化函数定义为类型属性。对于类型 T 的每个子类型 S,Ada 定义了以下与流相关函数和过程关联的属性

类型 输入 输出
简单 S'Read S'Write
简单,类范围 S'Class'Read S'Class'Write
复合 S'Input S'Output
复合,类范围 S'Class'Input S'Class'Output

我们将首先描述 S'Read 和 S'Write,因为它们是最简单的,从某种意义上说,是最“原始”的。

Write 属性

[编辑 | 编辑源代码]

过程 S'Write 在 13.13.2: 面向流的属性 (3) [带注释的] 中定义如下(请记住 S 是类型 T 的子类型)

  procedure S'Write(
        Stream : not null access Ada.Streams.Root_Stream_Type'Class;
        Item   : in  T);

S'Write 的职责是将 Item 转换为 Stream_Element 序列,并将结果写入 Stream。请注意,Stream 是对类范围类型 Root_Stream_Type'Class 的访问,因此程序员可以使用 S'Write 与从 Root_Stream_Type 派生的任何流类型一起使用。

根据 13.13.2: 面向流的属性 (9) [带注释的],Ada 为 S'Write 定义了默认实现,如下所示

  • 对于基本类型(例如,整数、字符、浮点数),默认实现将 Item 的适当表示写入 Stream。该表示是 *实现相关的*,但大多数情况下,这仅对应于内存中的表示。
  • 对于复合类型(例如,记录和数组),默认实现使用相应的 S'Write 过程写入每个组件(数组条目或记录组件)。请注意,没有写入其他信息。例如,如果 Item 是一个数组,则不会写入数组维度;如果 Item 有一个 *没有默认值* 的判别式,则不会写入判别式。从某种意义上说,S'Write 将 Item 的非常“原始”的表示写入 Stream。

显然,默认实现依赖于机器和编译器,只有当数据由使用相同编译器编译的程序写入和读取时,它才会有用。例如,如果数据要通过网络发送并由用另一种语言编写的程序读取,该程序运行在未知的架构上,那么程序员控制通过网络发送的数据格式非常重要。由于这种需要,Ada 允许程序员使用属性定义子句 (RM 13.3 [Annotated]) 覆盖 S'Write(以及以下描述的其他与流相关的函数)。

    for S'Write use user_defined_subprogram;

例如,假设网络协议要求以以下文本长度-类型-值格式格式化数据

  • 整数值格式化为 "<len> i <value>",其中 <len> 是用于表示整数的数字位数,<value> 是以十进制表示的整数。(例如,整数 42 将表示为 "2i42")

以下代码为整数情况定义了一个合适的 S'Write 过程(注意:为了简单起见,以下代码假设每个 Stream_Element 长度为 8 位)

   package Example is
       type Int is new Integer;
       type Int_Array is array (Int range <>) of Int;
      
       procedure Print (
            Stream : not null access Ada.Streams.Root_Stream_Type'Class;
            Item   : in  Int);
       
       for Int'Write Use Print;
   end Example;
   package body Example is
     procedure Print (
          Stream : not null access Ada.Streams.Root_Stream_Type'Class;
          Item   : in  Int) 
     is
          -- Convert Item to String (with no trailing space)
          Value  : String := Trim(Int'Image(Item), Left);
          
          -- Convert Value'Length to String (with no trailing space)
          Len    : String := Trim(Integer'Image(Value'Length), Left);
     
          Descr  : String := Len & 'i' & Value;
          Buffer : Stream_Element_Array (1 .. Stream_Element_Offset (Descr'Length));
     begin 
          -- Copy Descr to Buffer
          for I in Buffer'Range loop
              Buffer (I) := Stream_Element (Character'Pos (Descr (Integer (I))));
          end loop; 
       
          -- Write the result to Stream
          Stream.Write(Buffer);
     end Print;
   end Example;

请注意 Print 的结构:首先,Item 在 Stream_Element 序列(包含在 Buffer 中)中被“序列化”,然后通过调用 Write 方法(它将负责 Stream 上写入的详细信息)将此序列写入 Stream。现在假设有人想将 42 的描述打印到标准输出。可以使用以下代码

   with Ada.Text_IO.Text_Streams;
   use  Ada.Text_IO;  -- defines Current_Output. See RM A.10.1  [Annotated]
   
   -- Text_Streams.Stream (Current_Output) returns a stream access
   -- associated with the file given as parameter
   Int'Write (Text_Streams.Stream (Current_Output), 42); 

结果将是 "2i42" 打印到标准输出。请注意,以下代码

   Int_Array'Write (Text_Streams.Stream (Current_Output), (1=>42, 2=>128, 3=>6)); 

将写入标准输出字符串 "2i42_3i128_1i6"("_" 实际上不存在;它们已添加用于可读性),对应于按顺序对 42、128 和 6 调用 Int'Write。请注意,数组维度不会被写入。

如果有人想通过 TCP 连接发送相同的描述,可以使用以下代码(使用 GNAT)

    with GNAT.Sockets;
    use  GNAT;
    ...
    Sock   : Sockets.Socket_Type;
    Server : Sockets.Sock_Addr_Type := server address;
    ...
    Sockets.Create_Socket (Sock);
    Sockets.Connect_Socket (Sock, Server);
    
    -- Here Sock is connected to the remote server
    -- Use Sockets.Stream to convert Sock to a stream
    
    -- First send the integer 42
    Int'Write (Sockets.Stream (Sock), 42);
    
    -- Now send the array
    Int_Array'Write (Sockets.Stream (Sock), (1=>42, 2=>128, 3=>6));

读取属性

[edit | edit source]

过程 S'Read 在 13.13.2: Stream-Oriented Attributes (6) [Annotated] 中定义如下

  procedure S'Read(
        Stream : not null access Ada.Streams.Root_Stream_Type'Class;
        Item   : out  T);

它的行为与 S'Write 明显对称:S'Read 从 Stream 中读取一个或多个 Stream_Element,并将其“解析”以构造 Item。与 S'Write 的情况类似,Ada 为 S'Read 定义了默认实现,程序员可以使用属性定义子句来覆盖这些实现

  for S'Read use ...

例如,以下过程可以分配给类型 Int,其中for Int'Read use Parse;.

  procedure Parse (
                   Stream : not null access Root_Stream_Type'Class;
                   Item   : out Int)
  is
     Len    : Integer := 0;
     Buffer : Stream_Element_Array (1 .. 1);
     Last   : Stream_Element_Offset;
     Zero   : Stream_Element := Stream_Element (Character'Pos ('0'));
     Nine   : Stream_Element := Stream_Element (Character'Pos ('9'));
  begin
     -- Extract the length from the stream
     loop
        -- Read one element from the stream
        Stream.Read (Buffer, Last);
      
        exit when not (Buffer (1) in Zero .. Nine);
        Len := Len * 10 + Integer (Buffer (1) - Zero);
     end loop;
     
     -- Check for the correct delimiter
     if Character'Val (Integer (Buffer (1))) /= 'i' then
        raise Data_Error;
     end if;
  
     -- Now convert the following Len characters
     Item := 0;
     for I in 1 .. Len loop
        Stream.Read (Buffer, Last);
        Item := 10 * Item + Int (Buffer (1) - Zero);
     end loop;
  end Parse;

输出属性

[edit | edit source]

过程 S'Output13.13.2: Stream-Oriented Attributes (19) [Annotated] 中定义如下

  procedure S'Output(
        Stream : not null access Ada.Streams.Root_Stream_Type'Class;
        Item   : in  T);

S'Output 与 S'Write 的区别在于它的默认实现

  • 首先,它写入数组边界(如果 S 是数组)和判别式(如果 S 是记录)。
  • 然后,它调用 S'Write 来写入 Item 本身

请注意,边界或判别式是通过调用相应的 S'Write 过程写入的。因此,由于 Int_Array 在上面被定义为由 Int 索引的 Int 数组,因此以下行

    Int_Array'Output (Text_Streams.Stream (Current_Output), (1 => 42, 2 => 128, 3 => 6));

将产生("_" 添加用于可读性,实际上不在输出中存在)

    1i1_1i3_2i42_3i128_1i6

请注意行开头处的数组边界 "1i1" 和 "1i3"。

输入属性

[edit | edit source]

函数 S'Input13.13.2: Stream-Oriented Attributes [Annotated] 中定义如下

  function S'Input(
        Stream : not null access Ada.Streams.Root_Stream_Type'Class)
        return  T;

S'Input 对于 S'Read 的作用相当于 S'Output 对于 S'Write,因为 S'Read

  • 首先,它读取边界或判别式(使用相应的 S'Read
  • 它使用读取的值来创建要返回的对象
  • 它调用相应的 S'Read 来初始化对象

请注意,S'Input 是一个函数,而 S'Read 是一个过程。这与调用 S'Read 时,任何边界和/或判别式都必须已知,以便调用者可以创建对象并将其传递给 S'Read 一致。另一方面,对于 S'Input,边界/判别式是未知的,而是从流中读取的;因此,创建对象的责任在 S'Input 上。

类范围的读取和写入

[edit | edit source]

请注意,S'Read 和 S'Write 不是 S 的原始子程序,即使 S 是一个带标记的类型,它们也不能动态调度。为了允许 S'Read 和 S'Write 方法的动态调度,13.13.2: Stream-Oriented Attributes [Annotated] 定义了过程

  procedure S'Class'Write(
     Stream : not null access Ada.Streams.Root_Stream_Type'Class;
     Item   : in T'Class);
  
  procedure S'Class'Read(
     Stream : not null access Ada.Streams.Root_Stream_Type'Class;
     Item   : out T'Class);

请注意,在这两种情况下,Item 的类型都是 T'Class,因此 Item 可以是任何从 T 派生的类型。这些过程的行为是调度到由 Item 的标记标识的实际 S'Write 或 S'Read。有关类范围流属性 S'Class'Read 和 S'Class'Write 用法的示例,请参阅Ada Programming/Input Output/Stream Tutorial/Example

类范围的输入和输出

[edit | edit source]

类似于 S'Read 和 S'Write13.13.2: 面向流的属性 [注释] 定义了 S'Output 和 S'Input 的类级版本。

  procedure S'Class'Output(
     Stream : not null access Ada.Streams.Root_Stream_Type'Class;
     Item   : in T'Class)
  
  function S'Class'Input(
     Stream : not null access Ada.Streams.Root_Stream_Type'Class)
          return T'Class;

当我们记住带标记的类型实际上可以被视为具有“隐藏的判别式”的记录时,它们默认的行为几乎是显而易见的。

  • S'Class'Output 首先将标签转换为字符串,然后在结果上调用 String'Output,从而将标签写入流。接着,S'Class'Output 分派到由标签标识的特定类型的子程序 S'Output
  • S'Class'Input 首先通过调用 String'Input 并将结果转换为标签来从流中读取标签。接着,S'Class'Input 分派到由标签标识的特定类型的子程序 S'Input

有关更详细和精确的解释,请参阅 13.13.2: 面向流的属性 [注释]。有关类级流属性用法示例,请参阅 Ada Programming/Libraries/Ada.Streams/Example

规范

[edit | edit source]
--                     Standard Ada library specification
--   Copyright (c) 2003-2018 Maxim Reznik <[email protected]>
--   Copyright (c) 2004-2016 AXE Consultants
--   Copyright (c) 2004, 2005, 2006 Ada-Europe
--   Copyright (c) 2000 The MITRE Corporation, Inc.
--   Copyright (c) 1992, 1993, 1994, 1995 Intermetrics, Inc.
--   SPDX-License-Identifier: BSD-3-Clause and LicenseRef-AdaReferenceManual
-- -------------------------------------------------------------------------

package Ada.Streams is
   pragma Pure (Streams);

   type Root_Stream_Type is abstract tagged limited private;
   pragma Preelaborable_Initialization (Root_Stream_Type);

   type Stream_Element is mod implementation_defined;
   type Stream_Element_Offset is range
     implementation_defined .. implementation_defined;

   subtype Stream_Element_Count is
     Stream_Element_Offset range 0..Stream_Element_Offset'Last;

   type Stream_Element_Array is
     array (Stream_Element_Offset range <>) of aliased Stream_Element;

   procedure Read (Stream : in out Root_Stream_Type;
                   Item   : out Stream_Element_Array;
                   Last   : out Stream_Element_Offset) is abstract;

   procedure Write (Stream : in out Root_Stream_Type;
                    Item   : in Stream_Element_Array) is abstract;

private

   pragma Import (Ada, Root_Stream_Type);

end Ada.Streams;



另请参阅

[edit | edit source]

维基教科书

[edit | edit source]

外部示例

[edit source]


Ada 参考手册

[edit | edit source]

Ada 95

[edit | edit source]

Ada 2005

[edit | edit source]

Ada 2012

[edit | edit source]

开源实现

[edit | edit source]

FSF GNAT

drake

华夏公益教科书