Godot之StringName解析

类描述

在Godot中,StringName是唯一字符串的内置类型。

StringName 是不可变的字符串,用于唯一名称的通用表示(也叫“字符串内嵌”)。值相同的两个 StringName 是同一个对象。进行比较时比普通 String 要快很多。

对于需要 StringName 的方法,你通常可以只传 String,会自动进行转换,不过有时候你可能会想要提前使用 StringName 构造函数来构造 StringName,在 GDScript 中也可以用 &"example" 语法。

Godot中的NodePath,这是与此类似的概念,针对存储预解析的场景树路径设计,NodePath我们后面会进行解析。

String 的所有方法都在这个类中可用。它们会将 StringName 转换为字符串,返回的也是字符串。这样做效率非常低,应该只在需要字符串时使用。

注意:转换为布尔值时,空的 StringName(StringName(""))为 false,其他 StringName 均为 true。不能使用 not 运算符。请改用 is_empty 来检查空的 StringName。

核心思想概述

StringName内部实现了一个静态哈希表_table,将所有值相同的字符串,在_table中存储唯一一份,并对字符串值实现了引用计数,创建时+1,销毁时-1,为0时,从_table中移除对应节点,并释放字符串占用的内存。

关键代码

关键成员变量

_Data

_Data 是一个用于存储字符串的双端链表,支持引用计数、C string和String两种格式字符串。其他见代码注释。

	// 一个存储字符串的双端链表
	struct _Data {
		SafeRefCount refcount;
		// 静态引用次数
		SafeNumeric<uint32_t> static_count;
		// 所存储的字符串,可以以const char *和String两种形式存在
		const char *cname = nullptr;
		String name;
#ifdef DEBUG_ENABLED
		uint32_t debug_references = 0;
#endif
		String get_name() const { return cname ? String(cname) : name; }
		// 记录在_table中的索引
		int idx = 0;
		// 存储字符串的哈希值
		uint32_t hash = 0;
		// 前向节点指针
		_Data *prev = nullptr;
		// 后向节点指针
		_Data *next = nullptr;
		_Data() {}
	};

	// 在StringName变量中,只关心_data指向的节点,在静态_table中,才关心它的前向和后向节点
	_Data *_data = nullptr;

_table

_table定义为静态的目的,就是唯一且被所有StringName共享,从而实现在字符串相等时,可以共享一个字符串

enum {
	STRING_TABLE_BITS = 16,
	STRING_TABLE_LEN = 1 << STRING_TABLE_BITS,		// 静态_table数组大小
    STRING_TABLE_MASK = STRING_TABLE_LEN - 1		// 静态_table数组索引的掩码
};

// 静态_table,即被所有StringName共享,从而实现在字符串相等时,可以共享一个字符串
static _Data *_table[STRING_TABLE_LEN];

静态哈希表_table的原理图如下所示,也是StringName的精髓所在。

关键成员函数

构造函数

StringName::StringName(const String &p_name, bool p_static) {
	_data = nullptr;

	ERR_FAIL_COND(!configured);

	if (p_name.is_empty()) {
		return;
	}

	MutexLock lock(mutex);

	// 计算字符串的哈希值
	uint32_t hash = p_name.hash();
	// 计算在静态_table中的索引
	uint32_t idx = hash & STRING_TABLE_MASK;

	_data = _table[idx];

	while (_data) {
		// 相等的条件:哈希值相等 and 字符串相等
		if (_data->hash == hash && _data->get_name() == p_name) {
			break;
		}
		_data = _data->next;
	}

	// 如果找到,引用计数+1
	if (_data && _data->refcount.ref()) {
		// exists
		if (p_static) {
			_data->static_count.increment();
		}
#ifdef DEBUG_ENABLED
		if (unlikely(debug_stringname)) {
			_data->debug_references++;
		}
#endif
		return;
	}

	// 如果在静态_table中没有找到,则创建一个新的,并添加到_table
	_data = memnew(_Data);
	_data->name = p_name;
	_data->refcount.init();
	_data->static_count.set(p_static ? 1 : 0);
	_data->hash = hash;
	_data->idx = idx;
	_data->cname = nullptr;
	_data->next = _table[idx];
	_data->prev = nullptr;
#ifdef DEBUG_ENABLED
	if (unlikely(debug_stringname)) {
		// Keep in memory, force static.
		_data->refcount.ref();
		_data->static_count.increment();
	}
#endif

	if (_table[idx]) {
		_table[idx]->prev = _data;
	}
	_table[idx] = _data;
}

析构函数

void StringName::unref() {
	ERR_FAIL_COND(!configured);

	// _data有效 且 unref后,引用为0,才会进行释放
	if (_data && _data->refcount.unref()) {
		MutexLock lock(mutex);

		if (CoreGlobals::leak_reporting_enabled && _data->static_count.get() > 0) {
			if (_data->cname) {
				ERR_PRINT("BUG: Unreferenced static string to 0: " + String(_data->cname));
			} else {
				ERR_PRINT("BUG: Unreferenced static string to 0: " + String(_data->name));
			}
		}

		// 删除双向链表中的节点
		if (_data->prev) {
			_data->prev->next = _data->next;
		} else {
			if (_table[_data->idx] != _data) {
				ERR_PRINT("BUG!");
			}
			_table[_data->idx] = _data->next;
		}

		if (_data->next) {
			_data->next->prev = _data->prev;
		}
		// 释放内存
		memdelete(_data);
	}

	_data = nullptr;
}

	// 析构函数
	_FORCE_INLINE_ ~StringName() {
		if (likely(configured) && _data) { //only free if configured
			unref();
		}
	}

查找函数

StringName StringName::search(const char *p_name) {
	ERR_FAIL_COND_V(!configured, StringName());
	// 判断指针非空
	ERR_FAIL_NULL_V(p_name, StringName());
	// 字符串不以'\0'开头
	if (!p_name[0]) {
		return StringName();
	}

	MutexLock lock(mutex);

	//计算哈希值,进而计算在静态_table中的索引
	uint32_t hash = String::hash(p_name);
	uint32_t idx = hash & STRING_TABLE_MASK;

	_Data *_data = _table[idx];

	// 检索在链表中的结点
	while (_data) {
		// compare hash first
		if (_data->hash == hash && _data->get_name() == p_name) {
			break;
		}
		_data = _data->next;
	}

	// 节点有效 且 引用计数+1
	if (_data && _data->refcount.ref()) {
#ifdef DEBUG_ENABLED
		if (unlikely(debug_stringname)) {
			_data->debug_references++;
		}
#endif
		// 返回
		return StringName(_data);
	}

	return StringName(); //does not exist
}

赋值函数

// 赋值
void StringName::operator=(const StringName &p_name) {
	if (this == &p_name) {
		return;
	}

	// 赋新值,需先减少以前的字符的引用串数
	unref();

	// 引用计数+1
	if (p_name._data && p_name._data->refcount.ref()) {
		_data = p_name._data;
	}
}

一个重要的宏

SNAME(m_arg)用于优化 StringName(字符串名称)对象的创建。在许多编程场景中,频繁地创建和销毁同一字符串名称可能会对性能产生影响,特别是在高性能要求的场合。SNAME 宏通过内部的静态局部变量实现了一种高效的缓存机制,在首次使用时创建并存储特定字符串名称,后续调用时直接返回已创建的实例。

#define SNAME(m_arg) ([]() -> const StringName & { static StringName sname = _scs_create(m_arg, true); return sname; })()

SNAME 宏旨在提升高频率创建特定字符串名称场景下的性能,但在大多数情况下并不推荐滥用,仅在确实需要提高性能的关键路径上使用。

推荐在以下场景使用:

  • 在 Control::get_theme_() 和 Window::get_theme_() 等高频主题方法中;
  • 在 emit_signal(,..) 和 call_deferred(,..) 等信号关联的方法中;
  • 在重写 _set 和 _get 方法时与 StringName 进行比较的情况。

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
godot中的`class_name`关键字用于为类注册一个新的类型,并在编辑器中显示自定义图标。使用`class_name`关键字时,可以指定可选的图标路径,以使其在编辑器中显示。 以下是一个示例: ```gd # Item.gd extends Node class_name Item, "res://interface/icons/item.png" ``` 这个示例中,`Item.gd`文件中的类被注册为一个新的类型,并且使用`"res://interface/icons/item.png"`作为它在编辑器中的图标。 另外,godot中的类文件可以包含内部类。内部类使用`class`关键字来定义,并可以使用`ClassName.new()`函数来实例化。 以下是一个示例: ```gd # Inside a class file. # An inner class in this class file. class SomeInnerClass: var a = 5 func print_value_of_a(): print(a) # This is the constructor of the class file's main class. func _init(): var c = SomeInnerClass.new() c.print_value_of_a() ``` 在这个示例中,主类文件中包含了一个内部类`SomeInnerClass`,可以通过`SomeInnerClass.new()`来实例化并调用它的方法。 此外,以文件形式存储的类在godot中被视为资源。要在其他类中访问这些类资源,必须从磁盘加载它们。可以使用`load`函数或`preload`函数来加载类资源。 以下是一个示例: ```gd # Load the class resource when calling load(). var my_class = load("myclass.gd") # Preload the class only once at compile time. const MyClass = preload("myclass.gd") func _init(): var a = MyClass.new() a.some_function() ``` 在这个示例中,使用`load`函数来加载一个类资源,并使用`preload`函数在编译时预加载一个类资源。然后可以通过调用类对象上的`new`函数来实例化该类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值