18. Outils standards▲
Bien d'autres types sont fournis par la bibliothèque standard pour supporter des choses telles que :
- Les fils d'exécution ;
- Les canaux ;
- Les opérations sur le système de fichiers.
Ces types vont bien au-de-là de ce que les primitifs fournissent.
Voir aussi
Les primitifs et la bibliothèque standard.
18-1. Les fils d'exécution▲
Rust fournit un méchanisme de création de fils d'exécution natifs via la fonction spawn. L'argument de cette fonction est une closure transférable.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
use
std::thread;
static
NTHREADS: i32 = 10
;
// Ceci est le thread `main`.
fn
main() {
// On créé un vecteur pour récupérer tous les threads
// enfants qui ont été créés.
let
mut
children = vec!
[];
for
i in
0
..NTHREADS {
// On passe à un autre thread.
children.push(thread::spawn(move
|| {
println!
("this is thread number
{}
"
, i)
}));
}
for
child in
children {
// On attend que le thread se termine. Renvoie un résultat.
let
_ = child.join();
}
}
Ces threads seront programmés par le système d'exploitation.
18-2. Les canaux▲
Rust fournit les canaux asynchrones pour la communication entre les threads. Les canaux permettent l'envoi d'un flux unidirectionnel d'information entre deux extrémités : l'envoyeur (Sender) et le receveur (Receiver).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
use
std::sync::mpsc::{Sender, Receiver};
use
std::sync::mpsc;
use
std::thread;
static
NTHREADS: i32 = 3
;
fn
main() {
// Les canaux possèdent deux extrémités: l'envoyeur (`Sender<T>`) et le receveur (`Receiver<T>`),
// où `T` est le type du message à envoyer.
// (Le typage est optionnel)
let
(tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
for
id in
0
..NTHREADS {
// L'envoyeur peut être copié.
let
thread_tx = tx.clone();
// Chaque thread va envoyer son identifiant par le biais du canal.
thread::spawn(move
|| {
// Le thread prend l'ownership sur `thread_tx`.
// Chaque thread va ajouter un message dans le file d'attente
// dans le canal.
thread_tx.send(id).unwrap();
// L'envoi est une opération non-bloquante, le thread continuera à
// s'exécuter après l'envoi de son message.
println!
("thread
{}
finished"
, id);
});
}
// Ici, on récupère tous les messages.
let
mut
ids = Vec::with_capacity(NTHREADS as
usize);
for
_ in
0
..NTHREADS {
// La méthode `recv` choisit une message se trouvant dans le canal et
// gêlera le thread courant s'il n'y a aucun message.
ids.push(rx.recv());
}
// Montre l'ordre dans lequel les messages ont été envoyés.
println!
("
{:?}
"
, ids);
}
18-3. La structure Path▲
La structure Path représente les chemins de fichiers dans le système de fichiers sous-jacent. Il y a deux variantes de Path :
- posix::Path, pour les systèmes UNIX-like ;
- windows::Path, pour Windows.
Le prélude exporte la variante de Path adaptée à la plateforme.
Une instance de Path peut être créée à partir du type OsStr et fournit de nombreuses méthodes pour obtenir des informations à propos du fichier/répertoire sur lequel le chemin pointe.
Notez que, en interne, un objet Path n'est pas représenté par une chaîne de caractères UTF-8 mais est stocké dans un vecteur d'octets (Vec<u8>). En conséquence, la conversion d'un objet Path en &
str n'est pas gratuite et peut échouer (un objet Option
est renvoyé).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
use
std::path::{Path, PathBuf};
use
std::ffi;
fn
main() {
// Création d'un objet `Path` à partir d'une `&'static str`.
let
path = Path::new("."
);
// La méthode `display` renvoie une structure présentable.
let
display = path.display();
// La méthode `join()` fusionne un chemin avec toutes les ressources
// possédant une implémentation de la méthode `as_ref()` pour le type `Path` et
// renvoie un nouveau chemin avec le séparateur spécifique à l'OS.
let
new_path = path.join("a"
).join("b"
);
// Convertit le chemin en une vue sur une string.
match
new_path.to_str() {
None
=> panic!
("new path is not a valid UTF-8 sequence"
),
Some
(s) => println!
("new path is
{}
"
, s),
}
}
N'hésitez pas à cosnulter les autres méthodes de Path et la structure Metadata.
Voir aussi
18-4. La structure File▲
La structure File représente un fichier qui a été ouvert (contient un descripteur de fichier), et donne les accès lecture et/ou écriture sur le fichier sous-jacent.
Puisque de nombreuses choses peuvent mal se passer lorsqu'une opération est effectuée, toutes les méthodes de File renvoie le type io::Result
<T>, lequel étant un alias pour Result
<T, io::Error>.
Ceci couvre les potentielles erreurs de toutes les opérations d'entrée/sortie explicites. Grâce à cela, le programmeur peut visualiser toutes les erreurs possibles et est encouragé à les anticiper.
18-4-1. La méthode open▲
La méthode statique open() peut être utilisé pour ouvrir un fichier en lecture seule.
Un objet File est responsable d'une ressource, du descripteur de fichier et prend soin de fermer le fichier lorsqu'il est libéré.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
// Dans le fichier open.rs
use
std::error::Error;
use
std::fs::File;
use
std::io::prelude::*;
use
std::path::Path;
fn
main() {
// Crée un chemin vers le fichier désiré.
let
path = Path::new("hello.txt"
);
let
display = path.display();
// Ouvre le chemin en lecture seule, renvoie un objet `io::Result<File>`.
let
mut
file = match
File::open(&
path) {
// La méthode `description` de `io::Error` renvoie une chaîne de caractères
// qui décrit l'erreur.
Err
(why) => panic!
("couldn't open
{}
:
{}
"
, display,
why.description()),
Ok
(file) => file,
};
// Lit le contenu du fichier dans une chaîne de caractères, renvoie un objet `io::Result<usize>`.
let
mut
s = String::new();
match
file.read_to_string(&
mut
s) {
Err
(why) => panic!
("couldn't read
{}
:
{}
"
, display,
why.description()),
Ok
(_) => print!
("
{}
contains:
\n{}
"
, display, s),
}
// `file` sort du contexte, le flux ouvert sur le fichier "hello.txt"
// va être fermé.
}
Voici le résultat attendu :
2.
3.
4.
$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt contains:
Hello World!
Nous vous encourageons à confronter l'exemple précédent à des cas d'échec différents (e.g. hello.txt n'existe pas, hello.txt ne peut pas être lu).
18-4-2. La méthode create▲
La méthode statique create() ouvre un fichier en écriture seule. Si le fichier existe déjà, le contenu sera écrasé. Autrement, un nouveau fichier sera créé.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
// Dans le fichier create.rs
static
LOREM_IPSUM: &
'static
str =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"
;
use
std::error::Error;
use
std::io::prelude::*;
use
std::fs::File;
use
std::path::Path;
fn
main() {
let
path = Path::new("out/lorem_ipsum.txt"
);
let
display = path.display();
// Ouvre un fichier en écriture seule, renvoie un objet `io::Result<File>`.
let
mut
file = match
File::create(&
path) {
Err
(why) => panic!
("couldn't create
{}
:
{}
"
,
display,
why.description()),
Ok
(file) => file,
};
// Écrit la chaîne de caractères de `LOREM_IPSUM` dans `file`, renvoie
// un objet `io::Result<()>`.
match
file.write_all(LOREM_IPSUM.as_bytes()) {
Err
(why) => {
panic!
("couldn't write to
{}
:
{}
"
, display,
why.description())
},
Ok
(_) => println!
("successfully wrote to
{}
"
, display),
}
}
Voici le résultat attendu :
2.
3.
4.
5.
6.
7.
8.
9.
10.
$ mkdir out
$ rustc create.rs && ./create
successfully wrote to out/lorem_ipsum.txt
$ cat out/lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Comme pour l'exemple précédent, nous vous encourageons à confronter l'exemple à d'autres cas où l'opération pourrait échouer.
Il existe également d'autres outils pouvant ouvrir des fichiers dans des modes différents tels que : read+write, append, etc.
18-5. Les sous-processus▲
La structure process::Output représente la sortie d'un sous-processus terminé et la structure process::Command est un constructeur de processus.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
use
std::process::Command;
fn
main() {
let
output = Command::new("rustc"
)
.arg("--version"
)
.output().unwrap_or_else(|e| {
panic!
("failed to execute process:
{}
"
, e)
});
if
output.status.success() {
let
s = String::from_utf8_lossy(&
output.stdout);
print!
("rustc succeeded and stdout was:
\n{}
"
, s);
} else
{
let
s = String::from_utf8_lossy(&
output.stderr);
print!
("rustc failed and stderr was:
\n{}
"
, s);
}
}
Nous vous encourageons à essayer l'exemple précédent en lançant rustc avec un flag incorrect.
18-5-1. Les pipes▲
La structure Process représente un processus en cours d'exécution et expose la gestion de stdin, stdout et stderr pour interagir avec le processus sous-jacent via les pipes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
use
std::error::Error;
use
std::io::prelude::*;
use
std::process::{Command, Stdio};
static
PANGRAM: &
'static
str =
"the quick brown fox jumped over the lazy dog
\n
"
;
fn
main() {
// On crée un processus dans lequel la commande `wc` va s'exécuter.
let
process = match
Command::new("wc"
)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
Err
(why) => panic!
("couldn't spawn wc:
{}
"
, why.description()),
Ok
(process) => process,
};
// On écrit quelque chose dans l'entrée standard de `wc`.
// `stdin` est de type `Option<ChildStdin>`, mais puisque nous savons que
// cette instance en possède une, nous pouvons directement l'`unwrap()`.
match
process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
Err
(why) => panic!
("couldn't write to wc stdin:
{}
"
,
why.description()),
Ok
(_) => println!
("sent pangram to wc"
),
}
// Puisque `stdin` ne survit pas après l'appel du dessus, elle va être libérée et
// et le pipe fermé.
//
// C'est très important sinon `wc` ne pourrait pas commencer à traiter l'entrée
// que nous avons soumis.
// La champ `stdout` est également de type `Option<ChildStdout>` et doit donc être `unwrap()`.
let
mut
s = String::new();
match
process.stdout.unwrap().read_to_string(&
mut
s) {
Err
(why) => panic!
("couldn't read wc stdout:
{}
"
,
why.description()),
Ok
(_) => print!
("wc responded with:
\n{}
"
, s),
}
}
18-5-2. La méthode wait▲
Vous souhaiteriez peut-être attendre qu'un processus, dont un objet process::Child est responsable, se termine. Pour cela vous devez appeler la méthode Child::wait qui renverra un objet process::ExitStatus.
2.
3.
4.
5.
6.
7.
8.
9.
// Dans le fichier wait.rs
use
std::process::Command;
fn
main() {
let
mut
child = Command::new("sleep"
).arg("5"
).spawn().unwrap();
let
_result = child.wait().unwrap();
println!
("reached end of main"
);
}
2.
3.
4.
$
rustc wait.rs &&
./wait
reached end of main
# `wait` s'est exécuté pendant 5 secondes.
# Une fois la commande `sleep 5` terminée notre programme `wait` a pris fin.
18-6. Opérations sur le système de fichiers▲
Le module std::io::fs contient de nombreuses fonctions traitant avec le système de fichiers.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
// Dans le fichier fs.rs
use
std::fs;
use
std::fs::{File, OpenOptions};
use
std::io;
use
std::io::prelude::*;
use
std::os::unix;
use
std::path::Path;
// Une simple implémentation de la commande `cat path`.
fn
cat(path: &
Path) -> io::Result
<String> {
let
mut
f = try!
(File::open(path));
let
mut
s = String::new();
match
f.read_to_string(&
mut
s) {
Ok
(_) => Ok
(s),
Err
(e) => Err
(e),
}
}
// Une simple implémentation de la commande `echo s > path`.
fn
echo(s: &
str, path: &
Path) -> io::Result
<()> {
let
mut
f = try!
(File::create(path));
f.write_all(s.as_bytes())
}
// Une simple implémentation de la commande `touch path` (ignore les fichiers existants).
fn
touch(path: &
Path) -> io::Result
<()> {
match
OpenOptions::new().create(true
).write(true
).open(path) {
Ok
(_) => Ok
(()),
Err
(e) => Err
(e),
}
}
fn
main() {
println!
("`mkdir a`"
);
// Crée un répertoire et renvoie un objet `io::Result<()>`.
match
fs::create_dir("a"
) {
Err
(why) => println!
("!
{:?}
"
, why.kind()),
Ok
(_) => {},
}
println!
("`echo hello > a/b.txt`"
);
// Le match précédent peut être simplifié en utilisant la méthode `unwrap_or_else()`.
echo("hello"
, &
Path::new("a/b.txt"
)).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
println!
("`mkdir -p a/c/d`"
);
// Crée un répertoire récursivement, renvoie un objet `ìo::Result<()>`.
fs::create_dir_all("a/c/d"
).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
println!
("`touch a/c/e.txt`"
);
touch(&
Path::new("a/c/e.txt"
)).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
println!
("`ln -s ../b.txt a/c/b.txt`"
);
// Crée un lien symbolique, renvoie un objet `io::Result<()>`.
if
cfg!
(target_family = "unix"
) {
unix::fs::symlink("../b.txt"
, "a/c/b.txt"
).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
}
println!
("`cat a/c/b.txt`"
);
match
cat(&
Path::new("a/c/b.txt"
)) {
Err
(why) => println!
("!
{:?}
"
, why.kind()),
Ok
(s) => println!
(">
{}
"
, s),
}
println!
("`ls a`"
);
// Lit le contenu d'un répertoire, renvoie un objet `io::Result<Vec<Path>>`.
match
fs::read_dir("a"
) {
Err
(why) => println!
("!
{:?}
"
, why.kind()),
Ok
(paths) => for
path in
paths {
println!
(">
{:?}
"
, path.unwrap().path());
},
}
println!
("`rm a/c/e.txt`"
);
// Supprime un fichier, renvoie un objet `io::Result<()>`.
fs::remove_file("a/c/e.txt"
).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
println!
("`rmdir a/c/d`"
);
// Supprime un répertoire vide, renvoie un objet `io::Result<()>`.
fs::remove_dir("a/c/d"
).unwrap_or_else(|why| {
println!
("!
{:?}
"
, why.kind());
});
}
Voici le résultat attendu :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
$
rustc fs.rs &&
./fs
`mkdir a`
`echo hello
>
a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
>
hello
`ls a`
>
"a/b.txt"
>
"a/c"
`rm a/c/e.txt`
`rmdir a/c/d`
Et l'état final du répertoire a est :
2.
3.
4.
5.
6.
7.
$
tree a
a
|
-- b.txt
`-- c
`
-- b.txt ->
../b.txt
1
directory, 2
files
Voir aussi
18-7. Les arguments du programme▲
Les arguments passés en ligne de commande peuvent être récupérés en utilisant std::env::args qui renvoie un itérateur fournissant une String pour chaque argument :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
use
std::env;
fn
main() {
let
args: Vec<String> = env::args().collect();
// Le premier argument est le chemin à partir duquel le programme
// a été appelé.
println!
("My path is
{}
."
, args[0
]);
// Le reste des arguments sont ceux passés en ligne de commande au programme.
// On appelle le programme comme ceci:
// $ ./args arg1 arg2
println!
("I got
{:?}
arguments:
{:?}
."
, args.len() - 1
, &
args[1
..]);
}
$
./args 1
2
3
My path is ./args.
I got 3
arguments: ["1"
, "2"
, "3"
].
18-7-1. Récupération des arguments▲
Le pattern matching peut être utilisé pour traiter de simples arguments :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
use
std::env;
fn
increase(number: i32) {
println!
("
{}
"
, number + 1
);
}
fn
decrease(number: i32) {
println!
("
{}
"
, number - 1
);
}
fn
help() {
println!
("usage:
match_args <string>
Check whether given string is the answer.
match_args {{increase|decrease}} <integer>
Increase or decrease given integer by one."
);
}
fn
main() {
let
args: Vec<String> = env::args().collect();
match
args.len() {
// Pas d'arguments passés.
1
=> {
println!
("My name is 'match_args'. Try passing some arguments!"
);
},
// Un seul argument passé.
2
=> {
match
args[1
].parse() {
Ok
(42
) => println!
("This is the answer!"
),
_ => println!
("This is not the answer."
),
}
},
// Une commande et un argument passé.
3
=> {
let
cmd = &
args[1
];
let
num = &
args[2
];
// On traite le nombre.
let
number: i32 = match
num.parse() {
Ok
(n) => {
n
},
Err
(_) => {
println!
("error: second argument not an integer"
);
help();
return
;
},
};
// On traite la commande.
match
&
cmd[..] {
"increase"
=> increase(number),
"decrease"
=> decrease(number),
_ => {
println!
("error: invalid command"
);
help();
},
}
},
// On couvre tous les autres cas...
_ => {
// ... en affichant l'aide.
help();
}
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
$
./match_args Rust
This is not the answer.
$
./match_args 42
This is the answer!
$
./match_args do
something
error: second argument not an integer
usage:
match_args <
string>
Check whether given string is the answer.
match_args {increase|
decrease} <
integer>
Increase or decrease given integer by one.
$
./match_args do
42
error: invalid command
usage:
match_args <
string>
Check whether given string is the answer.
match_args {increase|
decrease} <
integer>
Increase or decrease given integer by one.
$
./match_args increase 42
43
18-8. FFI▲
Rust fournit une Interface pour Fonction Externe (« Foreign Function Interface », dans la langue de Shakespear) pour les bibliothèques écrites en C. Les fonctions externes peuvent être déclarées dans un bloc extern
annoté de l'attribut #[link]
contenant le nom de la bibliothèque externe.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
// Dans le fichier ffi.rs
use
std::fmt;
// Ce bloc externe lie la bibliothèque libm.
#[link(name =
"m"
)]
extern
{
// Ceci est une fonction externe
// qui calcule la racine carrée d'un nombre complexe à précision simple.
fn
csqrtf(z: Complex) -> Complex;
}
fn
main() {
// z = -1 + 0i
let
z = Complex { re: -1
., im: 0
. };
// Appeler une fonction externe est une opération dite "à risque".
let
z_sqrt = unsafe
{
csqrtf(z)
};
println!
("the square root of
{:?}
is
{:?}
"
, z, z_sqrt);
}
// Implémentation minimale d'un nombre complex à précision simple.
#[repr(C)]
#[derive(Clone, Copy)]
struct
Complex {
re: f32,
im: f32,
}
impl
fmt::Debug for
Complex {
fn
fmt(&
self
, f: &
mut
fmt::Formatter) -> fmt::Result
{
if
self
.im < 0
. {
write!
(f, "
{}
-
{}
i"
, self
.re, -self
.im)
} else
{
write!
(f, "
{}
+
{}
i"
, self
.re, self
.im)
}
}
}
$
rustc ffi.rs &&
./ffi
the square root of -1
+0i is 0
+1i
Puisque l'appel de fonctions externes est considéré comme « à risque », il est courant d'écrire des wrappers sécurisés.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
// Dans le fichier safe.rs
use
std::fmt;
#[link(name =
"m"
)]
extern
{
fn
ccosf(z: Complex) -> Complex;
}
// Wrapper sécurisé.
fn
cos(z: Complex) -> Complex {
unsafe
{ ccosf(z) }
}
fn
main() {
// z = 0 + 1i
let
z = Complex { re: 0
., im: 1
. };
println!
("cos(
{:?}
) =
{:?}
"
, z, cos(z));
}
// Implémentation minimale d'un nombre complexe à précision simple.
#[repr(C)]
#[derive(Clone, Copy)]
struct
Complex {
re: f32,
im: f32,
}
impl
fmt::Debug for
Complex {
fn
fmt(&
self
, f: &
mut
fmt::Formatter) -> fmt::Result
{
if
self
.im < 0
. {
write!
(f, "
{}
-
{}
i"
, self
.re, -self
.im)
} else
{
write!
(f, "
{}
+
{}
i"
, self
.re, self
.im)
}
}
}