< 返回版块

xleine 发表于 2025-11-27 16:21

之前一直想学习写 rust,最近刚好有一个契机开始第一次写代码。

具体来说我在做一个算卦小软件,其中有个结构 Gua64 用于对应到单个卦象的结果。

希望的结果是:

有个结构可以记录卦象的内容、卦名、注释、备注等等信息,作为一个基础结构。

因为卦象一共有 64 个,所以我定义了 64 个常量,希望在 new 的时候可以直接转为其中一个常量,从我的角度来讲感觉这样性能和结构会更加好?对于这种固定的数据不太希望每次都调用方法进行计算,感觉不太好。

但是接下来我就在想怎么在创建的时候将这些东西对应起来,但是没有什么特别好的想法,于是让 gemini 改了下看看结果:

gemini 的改动是去掉 name 属性,改为方法计算, new 的时候直接通过 match 进行匹配返回常量。然后提供了一个新的方法(from_parts)来创建新的结果,改后的示例如下:

    pub const 未济: Gua64 = Self::from_parts(Gua8::离, Gua8::坎);

    const fn from_parts(shang: Gua8, xia: Gua8) -> Self {
        Self { shang, xia }
    }

    pub const fn new(shang: Gua8, xia: Gua8) -> Self {
        match (shang, xia) {
            (Gua8::乾, Gua8::乾) => Self::乾,
            (Gua8::坤, Gua8::坤) => Self::坤,
        }
    }

    pub const fn parse_name(shang: Gua8, xia: Gua8) -> SharedString {
        match (shang, xia) {
            (Gua8::乾, Gua8::乾) => SharedString::new_static("乾"),
            (Gua8::坤, Gua8::坤) => SharedString::new_static("坤"),
        }
    }

parse_name 方法内部也是通过 match 来匹配的。

看上去这种写法好像还可以?但是比如对于 name 来说,其实和具体的常量是对应的,每次都 match 感觉有点奇怪。

我希望的是初始化出来的常量上面有完整的信息,直接用就好了。

顺着这个思路的话我感觉可以在 from_parts 里面将需要的信息(卦名、注释、备注)都初始化出来,然后 new 方法里面只需要传入两个基本的 Gua8 就可以。

这样的方向是对的吗?

PS:我在 gemini 改完后,继续问他 Gua64 是否有可以改进的地方,它的回复如下:

经过分析 src/core/models/gua64.rs,我发现了以下 3 个可以改进的地方:

移除 new 方法中的冗余 match:
现状:new 方法包含一个巨大的 match 表,用来返回预定义的常量。
问题:由于 Gua64 已经是 Copy 类型(只包含两个 u8 大小的枚举),直接构造一个新的实例(Self { shang, xia })在内存上与返回常量是完全等价的。目前的 match 表不仅没有性能优势,反而增加了 CPU 的分支预测开销和代码维护成本。
建议:删除 match,直接返回 Self { shang, xia }。同时删除不再需要的 from_parts 辅助函数。
解耦 gpui 依赖:
现状:文件引入了 gpui::SharedString。
问题:这违反了核心逻辑层(Core)应独立于 UI 框架(GPUI)的原则。
建议:将 SharedString 替换为 Rust 标准库的 &'static str(对于固定名称)或 String(对于动态拼接)。鉴于卦名都是固定的,&'static str 是最佳选择。
优化 parse_name (可选):
现状:使用 64 分支的 match 匹配名称。
建议:虽然可以改为查表法,但考虑到代码可读性和 Rust 编译器的优化能力,目前的 match 形式其实是可以接受的。主要改进点还是在于返回类型改为 &'static str。
总结建议方案:

修改 new 为直接构造。
将 SharedString 替换为 &'static str,移除 gpui 依赖。

感觉他好像在推翻上次的修改?按照这样的逻辑的话其实就相当于常量没有用了,那么最终后续准备添加的一些信息就需要通过方法实时计算了。

所以总结一下,问题如下:

  1. 我这 把已知数量的结构初始化出来为常量,在 new 的时候再去匹配到常量 的这种做法合理吗?
  2. 我这种场景还有什么其他的方案吗?
  3. 还是说我根本不应该定义常量?

PS:契机的话就是:

  1. 有个朋友有这个需求,所以做起来有点动力,难度也不大。
  2. ai 可以快速解决一些小问题,减少了我查询资料的时间
  3. gpui-component 节省了自己实现组件的时间,而且相比于 gpui 来说有个比较独立的例子来参考

PPS:目前代码肯定有很多问题,如果恰好看的的话可以帮忙指正,感谢!


Ext Link: https://github.com/loveloki/gua/blob/main/src/core/models/gua64.rs

评论区

写评论
作者 xleine 2025-11-28 21:10

谢谢大家的建议,我会根据建议再思考下怎么修改合适的。好像不能 @ 人,就不一一回复啦!

我考虑的主要倒不是性能,这里的 Gua64 这点性能肯定是怎么写影响都不大的。

我的观点是 可读性和可维护性更重要。只是刚开始写,因为对 rust 不熟悉,所以希望写得更加规范一点,一开始就有一个比较好的习惯会比较好。然后在实现的时候有时候就会陷入一些纠结,例如:这样写是不是不太好,还能怎么更好去实现吗等等。熟练了之后就不会有这种纠结了,这应该属于学习早期的“阵痛”吧。

所以位运算我是不会考虑的,可读性更加重要,这种程度的“极致优化”完全没有必要。

另一方面是不清楚 ai 给的建议会不会有什么坑,毕竟在我没有深入理解的情况下,更多是它确实能跑起来(当然我也在逐渐理解这些概念),但是用了 AI 后写代码有点快了,感觉我的理解能力跟不太上,所以才发了这个请教帖子。

宏也是刚刚有了一点了解,因为写的时候会感觉对于固定数量的场景应该输入输出更加固定,所以用了枚举,然后用了枚举后还需要做转入转出,刚开始是手写的,但是量上来后发现太痛苦了,就问了下AI,然后了解到了宏对于这种场景就很合适(另一方面也是因为手写的话需要手动写 !unreachable!(),感觉有点不够“优雅”)

不过看了大家的回复我也在思考是不是有点像太多了,总之边写边调整吧,再真的遇到问题之前考虑太多就是纯纯浪费时间了。

之前阅读基本知识也很久之前了,接下来我会再去读读,把一些基本类型和知识弄清楚,现在有的地方还是不太清楚,不过有了 AI 之后不是很清楚的也能写出来(也是个好事情

总之谢谢大家!

xiaoyaou 2025-11-28 18:57

如果喜欢倒腾的话,可以试试善用过程宏~~。下面例子的宏使用只是单纯简化替代手写的代码的繁琐和分散,使用宏在源码上可以将卦象的信息集中关联到一起,增强可读性,实际编译后的代码也是完全一样的

说个题外话,其实这样子做,有点费劲还舍本逐末了,不过闲下来喜欢专研的话,倒也不错,正常情况下做业务功能基本不可能过多考虑你说的那些顾虑的(&'static str这种朴素优化手段还是应该考虑一下的,顺手的事),直接各种struct结构体酷酷写就好了,这点性能损失在绝大部分情况下都可以忽略不计的。

然后补充一点,这里的方案之所以能编译优化,是因为使用了enum类型去编排卦象种类,虽然我们代码里也会存在一些match匹配,但是编译器可以轻松把它们优化为指针查找,不涉及任何分支跳转,这跟你提到的声明一个常量里面包含自己的固有属性信息是完全等效的,区别只是前者是用指针偏移去全局表查找,后者是用指针偏移在常量值局部(字段)查找。

而使用位运算,可以快速执行卦象的变化逻辑,直接消除了大部分的分支判断。这也得益于卦象本身也算是一种特殊的数值类型,很容易跟程序里的数值对应起来,可以方便地进行数值上的操作和转化。

举个栗子:

// 使用宏简化, `Gua`负责派生卦属性相关内容,`EnumU8`负责简化枚举和u8的相互转化
/// 利用u8布局,从低位开始,分别代表每一个的爻位
#[derive(Gua, EnumU8, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gua8 {
    #[gua(desc = "坤卦")] // 自动获得`name()`和`desc()`方法查表
    坤 = 0b000,
    #[gua(name = "艮")]   // 同理
    艮 = 0b001,
    ...
}
/// 注意爻的顺序是从下往上,对应u8布局就是bit从低到高,代表乐Gua8布局中的偏移位
#[derive(Clone, Copy, EnumU8)]
pub enum Gua8YaoIndex {
    /// 初爻(一爻)
    First = 0,
    /// 二爻
    Second = 1,
    /// 三爻
    Third = 2,
}

impl Gua8YaoIndex {
    /// 当前爻位代表的u8布局的位置掩码
    const fn mask(self) -> u8 {
        1 << self.as_u8()
    }
}
impl Gua8 {
    pub const MAX_YAO_NUM: u8 = 3;

    pub const YAO_MASK: u8 = 0b111;

    pub const fn new(yao1: Yao, yao2: Yao, yao3: Yao) -> Self {
        // 计算每个爻位,然后合并
        let yao1 = yao1.as_u8() << Gua8YaoIndex::First.as_u8();
        let yao2 = yao2.as_u8() << Gua8YaoIndex::Second.as_u8();
        let yao3 = yao3.as_u8() << Gua8YaoIndex::Third.as_u8();
        Self::from_u8_unchecked(yao1 | yao2 | yao3)
    }
    pub const fn reverse(&mut self, index: Gua8YaoIndex) {
        *self = Self::from_u8_unchecked(self.as_u8() ^ index.mask())
    }
}
/// Gua64与Gua8的派生用法一模一样
#[derive(Gua, EnumU8)]
pub enum Gua64 {...}

// 可以再单独实现其他方便使用的api
impl Gua8 {...}
impl Gua64 {
    pub const fn new(shang: Gua8, xia: Gua8) -> Self {
        Self::from_u8_unchecked(shang.as_u8() << Gua8::MAX_YAO_NUM | xia.as_u8())
    }
    pub const fn yao(self, index: Gua64YaoIndex) -> Yao {
        Yao::from_u8_unchecked((self.as_u8() >> index.as_u8()) & Yao::YAO_MASK)
    }
    pub const fn hu_gua(self) -> Self {
        let shang = Gua8::new(
            self.yao(Gua64YaoIndex::Third),
            self.yao(Gua64YaoIndex::Fourth),
            self.yao(Gua64YaoIndex::Fifth),
        );
        let xia = Gua8::new(
            self.yao(Gua64YaoIndex::Second),
            self.yao(Gua64YaoIndex::Third),
            self.yao(Gua64YaoIndex::Fourth),
        );
        Self::new(shang, xia)
    }
    pub const fn as_split(self) -> (Gua8, Gua8) {
        (
            Gua8::from_u8_unchecked(self.as_u8() >> Gua8::MAX_YAO_NUM),
            Gua8::from_u8_unchecked(self.as_u8() & Gua8::YAO_MASK),
        )
    }
    pub fn display(self) -> String {
        let (shang, xia) = self.as_split();
        format!("{}(上卦: {},下卦:{})",self.name(),shang.name(),xia.name())
    }
    pub const fn change(&mut self, index: Gua64YaoIndex) {
        *self = Self::from_u8_unchecked(self.as_u8() ^ index.mask())
    }
}
/// 为Enum自动派生gua相关属性查表方法
#[proc_macro_derive(Gua, attributes(gua))]
pub fn derive_gua(input: TokenStream) -> TokenStream {
    ...
    let output = quote! {
        impl #enum_ident {
            /// 返回卦的名称,默认为当前变体名,可以从`#[gua(name = ..., ...)]`宏属性中设置
            pub const fn name(&self) -> &'static str {
                match self {
                    #(#arms_name)*
                }
            }

            /// 返回卦的释义,默认为空,可以从`#[gua(desc= ..., ...)]`宏属性中设置
            pub const fn desc(&self) -> &'static str {
                match self {
                    #(#arms_desc)*
                }
            }
        }
    };
    output.into()
}
/// 为Enum派生as_u8,from_u8,from_u8_unchecked接口
#[proc_macro_derive(EnumU8)]
pub fn derive_enum_u8(input: TokenStream) -> TokenStream {
    ...
    let output = quote! {
        impl #enum_ident {
            pub const fn as_u8(self) -> u8 {
                self as u8
            }

            pub const fn from_u8(num: u8) -> Option<Self> {
                 match num {
                    #(#arms),*,
                    _ => None
                }
            }

            pub(crate) const fn from_u8_unchecked(num: u8) -> Self {
                 unsafe { std::mem::transmute(num) }
            }
        }
    };
    output.into()
}
mag1c1an1 2025-11-28 10:43

gemeni说的没错,其实可以用 https://godbolt.org/z/1nz1cjMca看看生成的汇编,在我看来合适的memory copy比分支跳转好的多,能够保证缓存。但是我不认为在现代计算设备上,这个Gua64会成为性能瓶颈。

xiaoyaou 2025-11-27 21:40

卦的种类都是已知固定的,这种情况下,极致优化的大杀器应该也就是enum + const fn + 位操作了吧。 追求极限性能的话,位操作是得要的;enum用于编排卦相关类型的种类,同时方便位转换和操作;const fn算是小优化一下编译期常量值计算;有了enum编排类型,用match模式匹配也方便很多,可读性上也不会影响,编译器会自动优化成查表跳转。

这样,可读性有了,极限性能也有了

#[derive(Debug, Clone, Copy)]
pub enum Yao {
    阴 = 0,
    阳 = 1,
}

impl Yao {
    pub const fn new(is_yang: bool) -> Self {
        match is_yang {
            true => Yao::阳,
            false => Yao::阴,
        }
    }

    pub const fn is_yang(&self) -> bool {
        matches!(self, Yao::阳)
    }
    ...
}

#[derive(Debug, Clone, Copy)]
pub enum Gua8YaoIndex {
    /// 初爻(一爻)
    First = 2,
    /// 二爻
    Second = 1,
    /// 三爻
    Third = 0,
}

impl Gua8YaoIndex {
    pub const fn mask(self) -> u8 {
        1 << self as u8
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gua8 {
    坤 = 0b000,
    艮 = 0b001,
    坎 = 0b010,
    巽 = 0b011,
    震 = 0b100,
    离 = 0b101,
    兑 = 0b110,
    乾 = 0b111,
}
impl Gua8 {
    pub const fn new(yao1: Yao, yao2: Yao, yao3: Yao) -> Self {
        let offset1 = Gua8YaoIndex::First as u8;
        let offset2 = Gua8YaoIndex::Second as u8;
        let offset3 = Gua8YaoIndex::Third as u8;
        let num = (yao1 as u8) << offset1 | (yao2 as u8) << offset2 | (yao3 as u8) << offset3;
        unsafe { std::mem::transmute(num) }
    }

    pub const fn name(&self) -> &'static str {
        match self {
            Gua8::坤 => "坤",
            Gua8::艮 => "艮",
            Gua8::坎 => "坎",
            Gua8::巽 => "巽",
            Gua8::震 => "震",
            Gua8::离 => "离",
            Gua8::兑 => "兑",
            Gua8::乾 => "乾",
        }
    }

    pub const fn from_num(num: u8) -> Option<Self> {
        match num {
            1 => Some(Gua8::坤),
            2 => Some(Gua8::艮),
            3 => Some(Gua8::坎),
            4 => Some(Gua8::巽),
            5 => Some(Gua8::震),
            6 => Some(Gua8::离),
            7 => Some(Gua8::兑),
            8 => Some(Gua8::乾),
            _ => None,
        }
    }

    pub const fn reverse(&mut self, index: Gua8YaoIndex) {
        let num = *self as u8;
        *self = unsafe { std::mem::transmute(num ^ index.mask()) };
    }
}

pub enum Gua64 {
    ...
}
...
Neutron3529 2025-11-27 19:17

如果是八卦的话 我建议直接

pub struct Gua64(i8);
pub struct Gua8(i8);

然后卦象名称含义什么的单独开const array 这样的好处是,Gua很容易做成copy的,而且占用空间足够小

1 共 5 条评论, 1 页