简体   繁体   中英

How to set correctly a generic collection type in rust

I am trying to test some language features in Rust 1.39 under FreeBSD 12 in comparison to Free Pascal 3.0.4 for a simple generic collection of points with 2D Points addressed by string keys. Unfortunately the code for the generic type declaration does not compile in a very early state and stops with:

error[E0106]: missing lifetime specifier
  --> src/main.rs:11:31
   |
11 |     type TPointMap = BTreeMap<&TString, TPoint>;
   |     

How do I have to rewrite the Rust code?

Details:

To test the language behavior I've written two small programs in Rust and Pascal addressing "syntactically" the same context. The Pascal program is a straightforward declaration of:

  1. some plain types, a record type and a generic map container,
  2. that will be used aftermath to define a point, store the point in a new allocated map,
  3. search the point by the key and write the data to the STDIO
  4. and finally free the map.
program test;
uses fgl; { Use the free pascal generics library }
type
   TDouble   = Double; { Define a 64 bit float } 
   TString   = String; { Define a string type  } 
   TPoint    = record  { Define a point record }
                  X : TDouble; { Coordinate X }
                  Y : TDouble; { Coordinate Y }
               end; 
   { Define a map of points with strings as key }
   TPointMap = specialize TFPGMap<TString, TPoint>;

{ Test program } 
var
   map   : TPointMap; { Declare the map variable }
   point : TPoint;    { Declare a point variable }
   found : TPoint;    { Varaiable for a found point }
   key   : TString;   { Variable to address the point in the map } 
begin
   map := TPointMap.create; { Allocate a new ma container }
   with point do begin      { Set the point variable }
      x := 1.0; y := 2.0;
   end;
   key := '123';              { Set the key address to '123'  }
   map.add(key,point);        { Store the point in the map }

   { Search the point an write the result in the rusty way }
   case map.TryGetData(key, found)  of
     true  : writeln('X: ',found.X:2;, ' Y:', found.Y:2:2);
     false : writeln('Key ''',key,''' not found');
   end;
   map.free;  { De-allocate the map }
   { Plain types are de-allocated by scope }
end.   

The program compiles and gives me:

$ ./main 
X: 1.00 Y:2.00

Here is my incorrect Rust version of the code:

use std::collections::BTreeMap; // Use a map from the collection 

type TDouble = f64; // Define the 64 bit float type
type TString = str; // Define the string type
struct TPoint {     // Define the string type
    x: TDouble,     // Coordinate X
    y: TDouble,     // Coordinate Y
}

// Define a map of points with strings as key 
type TPointMap = BTreeMap<&TString, TPoint>;

// Test program
fn main() {
    let point = TPoint { x: 1.0, y: 2.0 }; // Declare and define the point variable
    let mut map = TPointMap::new();        // Declare the map and allocate it
    let key: TString = "123";              // Declare and define the address of point 
    map.insert(&key, point);               // Add the point to the map 
    // search the point and print it
    match map.get(&key) {
        Some(found) => println!("X: {} Y: {}", found.X, found.y),
        None => println!("Key '{}' not found", key),
    }
   // map is de-allocated by scope
}

Remark: I'm aware due to the borrowing and ownership concept, that some code lines cannot be used in the Rust code. The line

 match map.get(&key)...

is one of them.

To be the rough equivalent of the freepascal version, TString should probably be String rather than str . A freepascal String is ( depending on some flags ) a pointer, a length and a heap-allocated array of characters. That's (pretty much) exactly what String is. str is just the array of characters and is unsized so it always has to be behind some kind of (fat) pointer.

Once that change is made, there are only a few other things to fix the code. TPointMap needs a lifetime parameter since it uses a reference type. The lifetime on the reference has to come from somewhere, so we make TPointMap generic in that lifetime.

type TPointMap<'a> = BTreeMap<&'a TString, TPoint>;

You might also consider simply using BTreeMap<TString, TPoint> if your use-case allows for it.

We need to do a bit of conversion to declare key: TString . String literals have type 'static str , but there's a simple to_string method to convert them to String s.

let key: TString = "123".to_string(); 

Finally, there's a typo in found.X .

Some(found) => println!("X: {} Y: {}", found.x, found.y),

Altogether, we have

use std::collections::BTreeMap; // Use a map from the collection

type TDouble = f64; // Define the 64 bit float type
type TString = String; // Define the string type
struct TPoint {
    // Define the string type
    x: TDouble, // Coordinate X
    y: TDouble, // Coordinate Y
}

// Define a map of points with strings as key
type TPointMap<'a> = BTreeMap<&'a TString, TPoint>;

// Test program
fn main() {
    let point = TPoint { x: 1.0, y: 2.0 }; // Declare and define the point variable
    let mut map = TPointMap::new(); // Declare the map and allocate it
    let key: TString = "123".to_string(); // Declare and define the address of point
    map.insert(&key, point); // Add the point to the map
                             // search the point and print it
    match map.get(&key) {
        Some(found) => println!("X: {} Y: {}", found.x, found.y),
        None => println!("Key '{}' not found", key),
    }
    // map is de-allocated by scope
}

(playground)

See also What are the differences between Rust's String and str ?

The reason you're seeing this failure is because you need a lifetime for the reference. Rust currently has no way of knowing how long your reference is supposed to last.

Once you fix that, you're going to run into the fact that you can't create variables of type str because it's unsized and the compiler has no way of telling at compile time how much space to allocate.

The simplest, most minimal change you can make here is to change the following lines:

type TString = &'static str;

and

type TPointMap = BTreeMap<TString, TPoint>;

(You'll also need to downcase the x in found.X , since Rust is case-sensitive.)

That will tell the compiler that your string type is &'static str , which is the type for string literals, since they live as long as the program.

You could also also make it work with str references of any arbitrary lifetime by doing the following instead:

type TString<'a> = &'a str;

and

type TPointMap<'a> = BTreeMap<TString<'a>, TPoint>;

However, in many programs, you may want to insert strings that aren't limited to string literals and avoid the necessary borrow for the lifetime of the map, such as if you want to return the map from a function. In such a case, it may make sense to use an owned String object so that the object lives as long as the map. In such a case, your code would look like the following:

use std::collections::BTreeMap; // Use a map from the collection

type TDouble = f64; // Define the 64 bit float type
type TString = String; // Define the string type
struct TPoint {     // Define the string type
    x: TDouble,     // Coordinate X
    y: TDouble,     // Coordinate Y
}

// Define a map of points with strings as key
type TPointMap = BTreeMap<TString, TPoint>;

// Test program
fn main() {
    let point = TPoint { x: 1.0, y: 2.0 }; // Declare and define the point variable
    let mut map = TPointMap::new();        // Declare the map and allocate it
    let key: TString = "123".to_string();  // Declare and define the address of point
    map.insert(key.clone(), point);        // Add the point to the map
    // search the point and print it
    match map.get(&key) {
        Some(found) => println!("X: {} Y: {}", found.x, found.y),
        None => println!("Key '{}' not found", key),
    }
   // map is de-allocated by scope
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM