Structures
5 min read
6 min read
3 min read
3 min read
3 min read
3 min read
3 min read
4 min read
5 min read
Struct Syntax
Slice supports structures containing one or more named fields of arbitrary type, including user-defined complex types. For example:
module M{ struct TimeOfDay { short hour; // 0 - 23 short minute; // 0 - 59 short second; // 0 - 59 }}This definition introduces a new type called TimeOfDay. Structure definitions form a scope, so the names of the structure fields need to be unique only within their enclosing structure.
Field definitions using a named type are the only construct that can appear inside a structure. It is impossible to, for example, define a structure inside a structure:
struct TwoPoints{ struct Point // Illegal! { short x; short y; } Point coord1; Point coord2;}This rule applies to Slice in general: type definitions cannot be nested (except for modules, which do support nesting). The reason for this rule is that nested type definitions can be difficult to implement for some target languages and, even if implementable, greatly complicate the scope resolution rules. For a specification language, such as Slice, nested type definitions are unnecessary – you can always write the above definitions as follows (which is stylistically cleaner as well):
struct Point{ short x; short y;}
struct TwoPoints // Legal (and cleaner!){ Point coord1; Point coord2;}Language Mapping
Slice structures map to C++ structures with the same name. For each Slice field, the C++ structure contains a public data member. For example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The Slice-to-C++ compiler generates the following definitions for this structure:
struct Employee{ std::int64_t number; std::string firstName; std::string lastName;
[[nodiscard]] std::tuple<const std::int64_t&, const std::string&, const std::string&> ice_tuple() const;};
std::ostream& operator<<(std::ostream& os, const Employee& value);For each field in the Slice definition, the C++ structure contains a corresponding public data member of the same name. Constructors are intentionally omitted so that the C++ structure qualifies as a plain old datatype (POD).
Comparison Operators
The generated C++ structures use templated comparison operators included from Ice.
// !=, <, <=, >, >= are implemented in the same mannertemplate< class T, std::enable_if_t< std::is_member_function_pointer_v<decltype(&T::ice_tuple)> && !std::is_polymorphic_v<T>, bool> = true>inline bool operator==(const T& lhs, const T& rhs){ return lhs.ice_tuple() == rhs.ice_tuple();}These operators compare the std::tuple returned by the generated ice_tuple() function.
Default Constructor
Structures have a default constructor that default-constructs each data member. Members having a complex type, such as strings, sequences, and dictionaries, are initialized by their own default constructor. However, the default constructor performs no initialization for members having one of the simple built-in types boolean, integer, floating point, or enumeration. For such a member, it is not safe to assume that the member has a reasonable default value. This is especially true for enumerated types as the member's default value may be outside the legal range for the enumeration, in which case an exception will occur during marshaling unless the member is explicitly set to a legal value.
To ensure that data members of primitive types are initialized to reasonable values, you can declare default values in your Slice definition. These default values are mapped to C++ data member initializers.
Printing Structs
The Slice compiler generates an operator<< that prints the C++ structure, including the value of all its data members:
std::ostream& operator<<(std::ostream& os, const Employee& value);You can suppress the generation of this operator, and tell the Slice compiler you’ll provide your own custom operator<<, with the "cpp:custom-print" metadata. For example:
// We'll provide our own custom operator<< for this struct.["cpp:custom-print"]struct Employee{ long number; string firstName; string lastName;}Ice for C# supports two different mappings for Slice structures. By default, Slice structures map to C# record structs if they (recursively) contain only value types. If a Slice structure (recursively) contains a string, proxy, class, sequence, or dictionary field, it maps to a record class. The "cs:class" metadata directive allows you to force the mapping to a record class for Slice structures that contain only value types.
In addition, for either mapping, you can control whether Slice fields are mapped to fields (the default) or to properties.
Mapping to Record Struct
Consider the following structure:
struct Point{ ["cs:identifier:X"] double x;
["cs:identifier:Y"] double y;}This structure consists of only value types and so, by default, maps to a C# partial record struct:
public partial record struct Point{ public double X; public double Y;
partial void ice_initialize();
public Point(double X, double Y) { this.X = X; this.Y = Y; ice_initialize(); }
public Point(Ice.InputStream istr) { this.X = istr.readDouble(); this.Y = istr.readDouble(); ice_initialize(); }}For each field in the Slice definition, the C# record struct contains a corresponding public field. This name of this public field is by default the name of the Slice field; here, we remapped the fields using the cs:identifier metadata directive.
The generated record has a primary constructor that allows you to construct and initialize a structure in a single statement:
var p = new Point(5.1, 7.8);The generated constructor calls the ice_initialize partial method after initializing the fields. You can customize this initialization by providing your own implementation of ice_initialize.
If you apply the cs:readonly metadata directive to the Slice struct, all the fields are mapped to readonly C# fields and the record struct is itself readonly. For example:
["cs:readonly"]struct ReadOnlyPoint{ ["cs:identifier:X"] double x;
["cs:identifier:Y"] double y;}maps to:
public readonly partial record struct ReadOnlyPoint{ public readonly double X; public readonly double Y; ...}Mapping to Record Class
Here is our Employee structure once more:
struct Employee{ ["cs:identifier:Number"] long number;
["cs:identifier:FirstName"] string firstName;
["cs:identifier:LastName"] string lastName;}The structure contains two strings, which are reference types, so the Slice-to-C# compiler generates a sealed partial record class for this structure:
public sealed partial record class Employee{ public long Number; public string FirstName = ""; public string LastName = "";
partial void ice_initialize();
public Employee() { ice_initialize(); }
public Employee(long Number, string FirstName, string LastName) { this.Number = Number; ArgumentNullException.ThrowIfNull(FirstName); this.FirstName = FirstName; ArgumentNullException.ThrowIfNull(LastName); this.LastName = LastName; ice_initialize(); }
public Employee(Ice.InputStream istr) { this.Number = istr.readLong(); this.FirstName = istr.readString(); this.LastName = istr.readString(); ice_initialize(); }}The generated record class provides the following constructors:
- a primary constructor with parameters for all the fields
- a constructor with parameters for fields with the following Slice types: Sequence, Dictionary, Struct mapped to record class in C# This constructor may be parameterless. It initializes string fields to the empty string, and other fields to their default value (typically
0,nullordefault; see Fields). - an “unmarshaling” constructor that unmarshals the record class from an InputStream
If you apply the cs:readonly metadata directive to the Slice struct, all the fields are mapped to readonly C# fields, except for fields with a Slice class type (they remain read-write).
Property Mapping
You can instruct the compiler to emit property definitions instead of public fields. For example:
["cs:property"] struct Point{ ["cs:identifier:X"] double x;
["cs:identifier:Y"] double y;}The cs:property metadata directive causes the compiler to generate a property for each Slice field:
public partial record struct Point{ public double X { get; set; } public double Y { get; set; }
// ... // same as without cs:property}If you add the cs:readonly metadata directive to your Slice struct, the generated properties are get-only, except for fields with a Slice class type (the mapped properties remain get-set).
A Slice structure maps to a Java class with the same name. For each Slice field, the Java class contains a corresponding public field. For example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The Slice-to-Java compiler generates the following definition for this structure:
public final class Employee implements java.lang.Cloneable, java.io.Serializable { public long number; public String firstName; public String lastName;
public Employee() { this.firstName = ""; this.lastName = ""; }
public Employee(long number, String firstName, String lastName) { this.number = number; this.firstName = firstName; this.lastName = lastName; }
@Override public boolean equals(java.lang.Object rhs) ...
@Override public int hashCode() ...
@Override public Employee clone() ...}You can optionally customize the mapping for fields to use getters and setters instead.
The equals method compares two structures for equality. Note that the generated class also provides the usual hashCode and clone methods. (clone has the default behavior of making a shallow copy.)
Generated Constructors
The mapped Java class provides two constructors:
- canonical constructor with parameters for all the fields
- a parameterless constructor that initializes all fields to default values (see Fields)
A Slice structure maps to a JavaScript class with the same name. For each Slice field, the JavaScript instance contains a corresponding field. As an example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The mapping for this structure is equivalent to the following JavaScript code:
// Generated JavaScript code
class Employee { constructor(number = 0n, firstName = "", lastName = "") { this.number = number; this.firstName = firstName; this.lastName = lastName; }}// Generated TypeScript definition
class Employee { constructor(number?: bigint, firstName?: string, lastName?: string); clone():Employee; equals(other: any): boolean; hashCode(): number;
number:bigint; firstName:string; lastName:string;}The generated class defines an equals method for comparison purposes and a clone method to create a shallow copy. For structures that are also legal dictionary key types, the mapped class also defines a hashCode function as required by the Ice.HashMap type.
Generated Constructor
The generated constructor has one parameter for each field. This allows you to construct and initialize an instance in a single statement (instead of first having to construct the instance and then assign to its fields).
All these parameters have also default values (see Fields).
A Slice structure maps to a MATLAB value class containing a public property for each field of the structure. For example, here is our Employee structure once more:
struct Employee{ ["matlab:identifier:Number"] long number;
["matlab:identifier:FirstName"] string firstName;
["matlab:identifier:LastName"] string lastName;}The MATLAB mapping generates the following definition for this structure:
classdef Employee properties Number (1, 1) int64 FirstName (1, :) char LastName (1, :) char end methods function obj = Employee(Number, FirstName, LastName) ... end end ...endGenerated Constructor
The generated constructor has one parameter for each property. You must either call this constructor with no arguments or with arguments for all the properties.
If you call the generated constructor with no argument, the constructor assigns default values to all properties (see Fields).
A Slice structure maps to a PHP class containing a public variable for each field of the structure. For example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The PHP mapping generates the following definition for this structure:
class Employee{ public $number; public $firstName; public $lastName;
public function __construct($number=0, $firstName='', $lastName=''); public function __toString();}The mapping includes a definition for the __toString magic method, which returns a string representation of the structure.
Generated Constructor
The generated constructor has one parameter for each field. This allows you to construct and initialize an instance in a single statement (instead of first having to construct the instance and then assign to its variables).
All these parameters have also default values (see Fields).
A Slice structure maps to a Python dataclass with the same name. For each Slice field, the Python dataclass contains a corresponding field. For example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The Python mapping generates the following definition for this structure:
@dataclass(order=True, unsafe_hash=True)class Employee: number: int = 0 firstName: str = "" lastName: str = ""All mapped fields have default values, such as 0 and the empty string (see Fields for details).
For structures that are also legal dictionary key types, the mapped dataclass is configured with order=True and unsafe_hash=True, as shown in our example above. The hashing is “unsafe” because the mapped dataclass is not frozen.
A Slice structure maps to a Ruby class with the same name. For each Slice field, the Ruby class contains a corresponding instance variable as well as accessors to read and write its value. For example, here is our Employee structure once more:
struct Employee{ long number; string firstName; string lastName;}The Ruby mapping generates the following definition for this structure:
class Employee attr_accessor :number, :firstName, :lastName
def initialize(number=0, firstName='', lastName='') @number = number @firstName = firstName @lastName = lastName end
def hash # ... end
def ==(other) # ... end
def inspect # ... endendThe compiler generates a definition for the hash method, which allows instances to be used as keys in a hash collection. The hash method returns a hash value for the structure based on the value of its instance variables.
The == method returns true if all instance variables of two structures are (recursively) equal.
The inspect method returns a string representation of the structure.
Generated Constructor
The generated constructor has one parameter for each field. This allows you to construct and initialize an instance in a single statement (instead of first having to construct the instance and then assign to its attributes).
All these parameters have also default values (see Fields).
A Slice structure maps to a Swift structure when this Slice structure does not have (recursively) any Slice class field. Conversely, a Slice structure maps to a Swift class when this Slice structure has (recursively) one or more Slice class field.
Mapping to Swift Struct
Consider the following Slice structure:
struct Point{ double x; double y;}This simple structure does not have any Slice class field so it maps to a public Swift structure:
public struct Point { public var x: Double = 0 public var y: Double = 0
public init() {}
public init(x: Double, y: Double) { self.x = x self.y = y }}For each field in the Slice definition, the Swift structure contains a corresponding public stored property of the same name.
When all the stored properties of the generated Swift structure are Hashable, the generated structure is itself hashable. For example:
struct TimeOfDay{ short hour; short minute; short second;}The corresponding Swift structure conforms to Hashable:
public struct TimeOfDay: Hashable { public var hour: Int16 = 0 public var minute: Int16 = 0 public var second: Int16 = 0
public init() {}
public init(hour: Int16, minute: Int16, second: Int16) { self.hour = hour self.minute = minute self.second = second }}Mapping to Swift Class
A Slice structure with a field of a class type is mapped to a Swift class. Take the Entry structure below:
class Data{ ...}
struct Entry{ int key; Data value;}Entry is mapped to a public Swift class:
public class Entry { public var key: Int32 = 0 public var value: Data? = nil
public init() {}
public init(key: Int32, value: Data?) { self.key = key self.value = value }}For each field in the Slice definition, the Swift structure contains a corresponding public stored property of the same name. Fields with type class or proxy are mapped to Swift optionals: the mapped type for value in the example above is Data?.
Generated Initializers
The mapped Swift struct or class has always two public initializers:
- a memberwise initializer that initializes all properties explicitly
- a parameterless initializer that assigns default values to all properties (see Fields)