🌱
Dev Compendium
  • Ethereum
    • Solidity
      • EVM
      • Architecture
      • Execution Context
      • Transactions
      • Gas
      • Calldata, Memory & Storage
      • Gas Optimisation
      • Function Declarations
      • receive() & fallback()
      • CALL vs. DELEGATE CALL
    • Yul
      • Introduction
      • Types
      • Basic Operations
      • Storage
      • Memory
        • Arrays
        • Structs
        • Tuples, revert, keccak256
        • Logs and Events
        • Gotchas
        • abi.encode
      • Calldata
        • External Calls
        • Dynamic Length Inputs
        • Transferring Value
        • Receiving Contract Calls
      • Contracts in Yul
      • Other Yul Functions
    • Foundry
    • Security
      • Common Vulnerabilities
      • Best Practices
      • Development Workflow
      • Contract Migration
    • Auditing Tools
      • Slither
      • Mythril
      • Fuzzing
    • Upgradable Contracts
      • Upgrade Patterns
      • ERC-1967 Implementation
      • Deployment
    • MEV
    • Tooling
      • Chainlink
      • IPFS
      • Radicle
    • Frontend
      • Contract Hooks
      • Wallet Connection
        • wagmi.sh
        • Rainbow Kit
      • thirdweb
    • Protocol Research
      • Uniswap v2
      • Uniswap v3
      • Curve
      • GMX
  • Starkware
    • Fundamentals
    • Account Abstraction
    • Universal Deployer
    • Cairo 1.0
    • starknet.js
    • Security Model
  • Zero Knowledge
    • Group Theory
    • ECDSA
  • Rust
    • Basic Operations
    • Set up
    • Primitives
    • Control Flow
    • Mutability & Shadowing
    • Adding Behavior
    • Lifetimes
    • Std Library
  • SUI
    • Architecture
    • Consensus Mechanism
    • Local Node Setup
    • Sui Client CLI
    • Move Contracts
      • Move
      • Move.toml
      • Move.lock
      • Accessing Time in Sui Move
      • Set up Development Framework
      • Debug & Publish
      • Package Upgrades
      • Sui Move Library
      • Difference from Core Move
    • Object Programming
      • Object Basics
      • Using Objects
      • Immutable Objects
      • Object Wrapping
      • Dynamic Fields
      • Collections
      • Unit Testing
      • Deployment with CLI
  • NEAR
    • Architecture
    • Contract Standards
      • Fungible Token (NEP-141)
      • Non-Fungible Token (NEP-171)
      • Storage Management (NEP-145)
      • Events (NEP-297)
      • Meta-Transactions
    • Rust Contracts
      • Development Workflow
      • Smart Contract Layout
      • Storage Management
      • Events & Meta-transactions
      • Method Types
      • Upgrading Contracts
      • Unit Testing
    • NEAR Libraries
    • Environment Variables
    • Serialisation
    • Security Concepts
    • Collections
    • JS SDK
Powered by GitBook
On this page
  1. Rust

Adding Behavior

PreviousMutability & ShadowingNextLifetimes

Last updated 1 year ago

impl blocks

We can add behavior to individual types with a simple impl block:

`struct BasicGreeter { greeting: String, }

impl BasicGreeter { fn greet(&self, name: &str) { println!("{}, {name}!", self.greeting); } }

let g = BasicGreeter { greeting: "Welcome".to_string(), }; g.greet("John");`

Output:

Welcome, John!

The &self parameter is special: it causes a function to be a method, operating on an instance of a type, as opposed to an associated function, which does not necessarily operate on an instance. In object-oriented terms, functions that take a self parameter (or any of the variants) are like instance methods, and functions that do not are like static methods.

Traits

Traits are the primary form of abstraction in Rust. A trait describes a set of behaviors that a type implements. It’s very similar to an interface in a language like Java. Actually, we’ve been using some traits already, in a subtle sort of way.

Remember how to print things to the screen? println!(...)? As you probably noticed by now, we can print a bunch of different things. Numbers, strings, booleans, etc.

println!("{}, {}, {}, {}", "hello", 42, 3.14, true);

This is an example of abstraction: all of the different types all support the behavior of “being printed.”

In Rust, this behavior is described by the [Display](<https://doc.rust-lang.org/std/fmt/trait.Display.html>) trait, which is like adding a toString method to a class in Java.

All of these types (String, &str, u32, f32, bool, …) implement Display.

Let’s take a look at writing a trait and implementing it.

`// create a trait trait Greeter { fn greet(&self, name: &str); }

struct MorningGreeter;

// implement the trait on MorningGreeter impl Greeter for MorningGreeter { fn greet(&self, name: &str) { println!("Good morning, {name}!"); } }

struct EveningGreeter;

// implement the trait on EveningGreeter impl Greeter for EveningGreeter { fn greet(&self, name: &str) { println!("Good evening, {name}!"); } }

let g1 = MorningGreeter; g1.greet("Alice"); // -> Good morning, Alice!

let g2 = EveningGreeter; g2.greet("Bob"); // -> Good evening, Bob!`

This isn’t terribly interesting yet. Let’s spice it up some.

`fn greet_wizard<G: Greeter>(g: G) { g.greet("Gandalf"); }

greet_wizard(EveningGreeter); // -> Good evening, Gandalf!`

If you have too many generic parameters, or if the bounds are too complex, you can move them to a where clause to organize your function signature a little:

fn my_function<T>(t: T) where T: Send + Sync {}

If you don’t need to refer to the generic parameter by name, you can use a shorthand:

`fn greet_wizard(g: impl Greeter) { g.greet("Gandalf"); }

greet_wizard(EveningGreeter); // -> Good evening, Gandalf!`

This code is identical to the previous greet_wizard example, but it’s a little easier to read, since there’s no G generic parameter floating around.

Trait objects

  • Therefore, trait objects are usually used behind some form of pointer, either regular (&dyn Trait) or smart (Box<dyn Trait>).

Drop

Called automatically when an object goes out of scope. The main use of the Drop trait is to free the resources that the implementor instance owns.

This uses a to allow us to pass it any parameter that implements the Greeter trait. If you’re familiar with Java, etc., the angle bracket <> syntax might look familiar.

The colon in G: Greeter means “G implements Greeter.” You can specify multiple using a +, like so: T: Debug + Display.

There’s another way to accept parameters based on what traits they implement, as opposed to by concrete type. Using the dyn keyword, we can create a . It’s a bit of a hairy topic if you dive deeply into it, but for now, keep in mind the following properties:

dyn Trait is , meaning you usually can’t work with it directly, since the compiler cannot know how big it is.

Trait objects include a , which can make function access a tiny bit slower. Usually, Rust’s generics are preferred, since they can be optimized per-type, and also preserve type information across the codebase.

5
generic type parameter
trait bounds
trait object
unsized
vtable
monomorphized