在Rust中,如何在函数参数中使用协方差?

3
今天我学到了rust不支持对fn参数的协变性,只有返回类型是协变的。(请参见rust文档)
为什么我要在rust中了解这个事实?因为我尝试实现一个非常简单的游戏,其中我将逻辑、事件处理和绘图分别放在三个不同的函数中,但它们都在同一个玩家向量上操作。
如果这不可能,在rust中相比c#版本会有什么等效的方法?
在C#中,这很简单Fiddle。您可以定义一个接口Y,一个类X必须实现,并定义一个相应的委托,该委托要求以该接口Y的IEnumerable作为参数。现在,您可以在需要仅接口Y的不同方法之间共享X列表。
using System;
using System.Collections.Generic;


public interface Actionable{
    void Do();
}

public interface Drawable{
    void Draw();
}

public class Player: Drawable, Actionable{

    public void Do(){
        Console.WriteLine("Action");
    }

    public void Draw(){
        Console.WriteLine("Draw");
    }
}

public class Program
{
    public delegate void DrawHandler(IEnumerable<Drawable> obj);
    public delegate void LogicHandler(IEnumerable<Actionable> obj);

    public static void gameloop(DrawHandler draw,LogicHandler action){

        List<Player> list = new List<Player>(){
            new Player()
        };

        for(int rounds = 0; rounds < 500; rounds++){
            draw(list);
            action(list);
        }

    }
    public static void Main()
    {
        gameloop(
             list =>{
                foreach(var item in list){
                    item.Draw();
                }
            },
            list =>{
                foreach(var item in list){
                    item.Do();
                }
            }
        );
    }
}

虽然我很天真,但我尝试在 Rust 中做出类似的事情!

trait Drawable {
    fn draw(&self) {
        println!("draw object");
    }
}

trait Actionable {
    fn do_action(&self, action: &String) {
        println!("Do {}", action);
    }
}

#[derive(Debug)]
struct Position {
    x: u32,
    y: u32,
}
impl Position {
    fn new(x: u32, y: u32) -> Position {
        Position { x, y }
    }
}
#[derive(Debug)]
struct Player {
    pos: Position,
    name: String,
}

impl Player {
    fn new(name: String) -> Player {
        Player {
            name,
            pos: Position::new(0, 0),
        }
    }
}

impl Drawable for Player {
    fn draw(&self) {
        println!("{:?}", self);
    }
}

impl Actionable for Player {
    fn do_action(&self, action: &String) {
        println!("Do {} {}!", action, self.name);
    }
}

type DrawHandler = fn(drawables: &Vec<&dyn Drawable>) -> Result<(), String>;
type LogicHandler = fn(actions: &Vec<&dyn Actionable>) -> Result<(), String>;
type EventHandler = fn(events: &mut sdl2::EventPump) -> Result<bool, String>;

fn game_loop(
    window: &mut windowContext,
    draw_handler: DrawHandler,
    event_handler: EventHandler,
    logic_handler: LogicHandler,
) -> Result<(), String> {
    let mut objects: Vec<&Player> = Vec::new();

    objects.push(&Player::new("b".to_string()));

    while event_handler(&mut window.events)? {
        logic_handler(&objects)?; // Does Not work

        window.canvas.clear();

        draw_handler(&objects)?; // Does Not Work
        window.canvas.present();
        ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }

    Ok(())
}

如果这在rust中不可行,与c#版本相比,等价于什么?
我接受在rust中这是不可能的。我想知道在rust中使用的是什么代替。

你可以看一下 https://github.com/amethyst/amethyst,但是这个问题对于一个SO问题来说太过宽泛了,我不会称你的代码为“简单”,相反,我建议你保持简单,不要在需要之前尝试过多的抽象。 - Stargateur
1
@Stargateur 感谢提供链接。你确定这个问题太宽泛了,不适合在SO上发表吗?因为如果我看到像 https://dev59.com/pXVC5IYBdhLWcg3w7V73?rq=1 这样的问题,我认为这个也可以适用。 - ExOfDe
我不明白在遍历集合这样的基本问题和你需要一本书才能回答的问题之间的联系。你只是随便挑了一个C#问题。 - Stargateur
的确是这样。我认为我缺乏看到我的问题影响的技能。 - ExOfDe
2个回答

5
在Rust中,很少有事情是隐式完成的,包括您发现的强制转换。在这种情况下,将Vec<&T>转换为Vec<&dyn Trait>是不可能的(因为T != dyn Trait),因为特质对象的存储方式;它们的宽度为两个指针宽度,而普通引用的宽度为一个指针宽度。这意味着Vec的长度(以字节为单位)需要加倍。

我知道在rust中这是不可能的。我想知道在rust中使用的是什么替代方法。

如果您只使用一种类型的对象,则可以限制类型:

type DrawHandler = fn(drawables: &Vec<Player>) -> Result<(), String>;
type LogicHandler = fn(actions: &Vec<Player>) -> Result<(), String>;

然而,你的游戏中很可能不仅有玩家,而且你想包括其他方面。这可以通过以下几种方式实现:
- 使用枚举来表示每种类型的对象。然后,你的函数输入可以采用枚举类型的值:
enum GamePiece {
    Player(Player),
    Enemy(Enemy),
    Item(Item),
    //etc.
}
  • 使用一个可根据对象拥有的属性来管理任意对象的 ECS。一些存在于 Rust 中的 ECS 包括:

    通常,它们的使用将类似于以下内容:

struct DrawingComponent {
    buffers: Buffer
}
struct DirectionAI {
    direction: Vector
}
struct Position {
    position: Point
}

let mut world = World::new();
world.insert((DrawingComponent::new(), DirectionAI::new(), Position::new()));

for (pos, direction) in world.iter_over(<(&mut Position, &DirectionAI)>::query()) {
    pos.position += direction.direction;
}
for (pos, drawable) in world.iter_over(<&Position, &mut DrawingComponent>::query()) {
    drawable.buffers.set_position(*pos);
    draw(drawable);
}

在这个系统中,您是基于组件而不是类型进行工作的。这样,ECS可以非常快速和高效地存储和访问项目。

Rust中存在协变性,但它不是面向对象的协变性,而是生命周期上的协变性。Rust Nomicon涵盖了这个主题,因为它对于日常用户来说有点小众。

请注意,该部分中的表格涵盖了'aT以及在某些情况下的U的方差。对于TU,它们的方差在于它们可能具有的任何生命周期参数,而不是类型本身。也就是说,它描述了'bStruct<'b>中是变体(或不变),而不是描述了如何将Struct<'b>转换为dyn Trait + 'b


1
&Player作为&dyn Drawable处理,看起来像在超类型中使用子类型,但实际上它是一种类型转换(两者在内存中完全不同,在@Optimistic Peach更详细地解释了这一点)。
有了这个想法,Vec<Player>不能被强制转换为Vec<&dyn Drawable>,它必须被转换。带有显式转换的代码如下:
fn game_loop(
    draw_handler: DrawHandler,
    logic_handler: LogicHandler,
) -> Result<(), String> {
    let mut objects: Vec<Player> = Vec::new();

    objects.push(Player::new("b".to_string()));

    for i in 0..1 {
        let actionable = objects.iter().map(|v| v as &dyn Actionable).collect();
        logic_handler(&actionable)?; // Does work!

        let drawables = objects.iter().map(|v| v as &dyn Drawable).collect();
        draw_handler(&drawables)?; // Does work!
    }

    Ok(())
}

这只是演示将&Player转换为&dyn Drawable的后果 - 这不是解决问题的最佳方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接