我正在寻找类似于Map默认值的东西。
m = new Map();
//m.setDefVal([]); -- how to write this line???
console.log(m[whatever]);
现在的结果是未定义的,但是我希望得到一个空数组 []。
我正在寻找类似于Map默认值的东西。
m = new Map();
//m.setDefVal([]); -- how to write this line???
console.log(m[whatever]);
现在的结果是未定义的,但是我希望得到一个空数组 []。
function getMapValue(map, key) {
return map.get(key) || [];
}
// And use it like:
const m = new Map();
console.log(getMapValue(m, 'whatever'));
如果这不能满足您的需求,并且您真正需要一个具有默认值的 Map,请编写自己的 Map 类,如下所示:
class MapWithDefault extends Map {
get(key) {
if (!this.has(key)) {
this.set(key, this.default());
}
return super.get(key);
}
constructor(defaultFunction, entries) {
super(entries);
this.default = defaultFunction;
}
}
// And use it like:
const m = new MapWithDefault(() => []);
m.get('whatever').push('you');
m.get('whatever').push('want');
console.log(m.get('whatever')); // ['you', 'want']
Map
构造函数已经要求的,不是吗? - Matías Fidemraizerget
的另一种实现方式是使用 if (!this.get(key) this.set(key, this.default())
。这样可以方便地使用单行代码 m.get('key').push(newValue)
,但代价是 get
方法会使对象发生变化,这有点棘手。 - Yi Jiang根据我的需求,我认为使用一个扩展了普通Map并添加了额外方法的DefaultMap
类会更清晰。这非常好,因为它导致了更多声明式的代码。这意味着,当你声明一个新的Map
时,不仅声明了键和值的类型,还声明了默认值。
举个例子:
// Using a primitive as a default value:
const myMap1 = new DefaultMap<string, number>(123);
const myMap1Value = myMap1.getAndSetDefault("some_key"); // Value is 123
// Using a factory function to generate a default value:
const myMap2 = new DefaultMap<string, number, [foo: Foo]>(
(foo) => foo.bar
);
const foo = new Foo();
const myMap2Value = myMap2.getAndSetDefault("some_key", foo); // Value is foo.bar
import { isFunction, isPrimitive } from "../functions/types";
/**
* `DefaultMap` is a data structure that makes working with default values easier. It extends a
* `Map` and adds additional methods.
*
* It is a common pattern to look up a value in a `Map`, and then, if the value does not exist, set
* a default value for the key, and then return the default value. `DefaultMap` abstracts this
* operation away by providing the `getAndSetDefault` method.
*
* Using a `DefaultMap` is nice because it makes code more declarative, since you specify what the
* default value is alongside the types of the keys/values.
*
* When instantiating a new `DefaultMap`, you must specify default value as the first argument. (The
* default value is the initial value that will be assigned to every new entry in the
* `getAndSetDefault` method.) For example:
*
* ```ts
* // Initializes a new empty DefaultMap with a default value of "foo".
* const defaultMapWithString = new DefaultMap<string, string>("foo");
*
* const value = defaultMapWithString.getAndSetDefault("bar");
* // value is now "foo" and an entry for "bar" is now set.
* ```
*
* Sometimes, instead of having a static initial value for every entry in the map, you will want a
* dynamic initial value that is contingent upon the key or some other variable. In these cases, you
* can instead specify that the `DefaultMap` should run a function that will return the initial
* value. (This is referred to as a "factory function".) For example:
*
* ```ts
* // Initializes a new empty DefaultMap with a default value based on "someGlobalVariable".
* const factoryFunction = () => someGlobalVariable ? 0 : 1;
* const defaultMapWithFactoryFunction = new DefaultMap<string, string>(factoryFunction);
* ```
*
* Note that in TypeScript and Lua, booleans, numbers, and strings are "passed by value". This means
* that when the `DefaultMap` creates a new entry, if the default value is one of these 3 types, the
* values will be copied. On the other hand, arrays and maps and other complex data structures are
* "passed by reference". This means that when the `DefaultMap` creates a new entry, if the default
* value is an array, then it would not be copied. Instead, the same shared array would be assigned
* to every entry. Thus, to solve this problem, any variable that is passed by reference must be
* created using a factory function to ensure that each copy is unique. For example:
*
* ```ts
* // Initializes a new empty DefaultMap with a default value of a new empty array.
* const factoryFunction = () => [];
* const defaultMapWithArray = new DefaultMap<string, string[]>(factoryFunction);
* ```
*
* In the previous two examples, the factory functions did not have any arguments. But you can also
* specify a factory function that takes one or more arguments:
*
* ```ts
* const factoryFunction = (arg: boolean) => arg ? 0 : 1;
* const defaultMapWithArg = new DefaultMap<string, string, [arg: boolean]>(factoryFunction);
* ```
*
* Similar to a normal `Map`, you can also include an initializer list in the constructor as the
* second argument:
*
* ```ts
* // Initializes a DefaultMap with a default value of "foo" and some initial values.
* const defaultMapWithInitialValues = new DefaultMap<string, string>("foo", [
* ["a1", "a2"],
* ["b1", "b2"],
* ], );
* ```
*
* Finally, note that `DefaultMap` has the following additional utility methods:
*
* - `getAndSetDefault` - The method that is called inside the overridden `get` method. In most
* cases, you can use the overridden `get` method instead of calling this function directly.
* However, if a factory function was provided during instantiation, and the factory function has
* one or more arguments, then you must call this method instead (and provide the corresponding
* arguments).
* - `getDefaultValue` - Returns the default value to be used for a new key. (If a factory function
* was provided during instantiation, this will execute the factory function.)
* - `getConstructorArg` - Helper method for cloning the map. Returns either the default value or
* the reference to the factory function.
*/
export class DefaultMap<Key, Value, Args extends unknown[] = []> extends Map<
Key,
Value
> {
private defaultValue: Value | undefined;
private defaultValueFactory: FactoryFunction<Value, Args> | undefined;
/**
* See the main `DefaultMap` documentation:
* https://isaacscript.github.io/isaacscript-common/other/classes/DefaultMap
*/
constructor(
defaultValueOrFactoryFunction: Value | FactoryFunction<Value, Args>,
initializerArray?: Iterable<[Key, Value]>,
) {
const argIsPrimitive = isPrimitive(defaultValueOrFactoryFunction);
const argIsFunction = isFunction(defaultValueOrFactoryFunction);
if (!argIsPrimitive && !argIsFunction) {
error(
`Failed to instantiate a DefaultMap since the provided default value was of type "${typeof defaultValueOrFactoryFunction}". This error usually means that you are trying to use an array (or some other non-primitive data structure that is passed by reference) as the default value. Instead, return the data structure in a factory function, like "() => []". See the DefaultMap documentation for more details.`,
);
}
super(initializerArray);
if (argIsFunction) {
this.defaultValue = undefined;
this.defaultValueFactory = defaultValueOrFactoryFunction;
} else {
this.defaultValue = defaultValueOrFactoryFunction;
this.defaultValueFactory = undefined;
}
}
/**
* If the key exists, this will return the same thing as the normal `Map.get` method. Otherwise,
* it will set a default value for the provided key, and then return the default value.
*/
public getAndSetDefault(key: Key, ...args: Args): Value {
const value = super.get(key);
if (value !== undefined) {
return value;
}
const defaultValue = this.getDefaultValue(...args);
this.set(key, defaultValue);
return defaultValue;
}
/**
* Returns the default value to be used for a new key. (If a factory function was provided during
* instantiation, this will execute the factory function.)
*/
public getDefaultValue(...args: Args): Value {
if (this.defaultValue !== undefined) {
return this.defaultValue;
}
if (this.defaultValueFactory !== undefined) {
return this.defaultValueFactory(...args);
}
error("A DefaultMap was incorrectly instantiated.");
}
/**
* Helper method for cloning the map. Returns either the default value or a reference to the
* factory function.
*/
public getConstructorArg(): Value | FactoryFunction<Value, Args> {
if (this.defaultValue !== undefined) {
return this.defaultValue;
}
if (this.defaultValueFactory !== undefined) {
return this.defaultValueFactory;
}
error("A DefaultMap was incorrectly instantiated.");
}
}
// eslint-disable-next-line isaacscript/complete-sentences-jsdoc
/**
* A function that creates the default value for your `DefaultMap`. For example, if it was a
* `DefaultMap` containing maps, the factory function would be: `() => new Map()`
*/
export type FactoryFunction<V, Args extends unknown[]> = (...args: Args) => V;
/* eslint-disable @typescript-eslint/no-unused-vars */
function test() {
// Boolean
const myDefaultMapBoolean = new DefaultMap<string, boolean>(false);
const myDefaultMapBooleanFactory = new DefaultMap<string, boolean>(
() => false,
);
const myDefaultMapBooleanWithoutParams = new DefaultMap(false);
// Number
const myDefaultMapNumber = new DefaultMap<string, number>(123);
const myDefaultMapNumberFactory = new DefaultMap<string, number>(() => 123);
const myDefaultMapNumberWithoutParams = new DefaultMap(123);
// String
const myDefaultMapString = new DefaultMap<string, string>("foo");
const myDefaultMapStringFactory = new DefaultMap<string, string>(() => "foo");
const myDefaultMapStringWithoutParams = new DefaultMap("foo");
// Array
const myDefaultMapArray = new DefaultMap<string, string[]>(() => []);
const myDefaultMapArrayWithoutParams = new DefaultMap(() => []);
// Map
const myDefaultMapMap = new DefaultMap<string, Map<string, string>>(
() => new Map(),
);
const myDefaultMapMapWithoutParams = new DefaultMap(() => new Map());
}
/* eslint-enable @typescript-eslint/no-unused-vars */
这里是一个简单的 MapWithDefault
类,它扩展了内置的 Map
。
聪明的技巧在于使用 symbols 来指定默认值,从而得到一个美观且易读的语法:
(下面的片段使用 TypeScript 提供类型安全性,但这是可选的,如果您愿意,可以删除类型注释。)
mapWithDefault.ts
:
export const DEFAULT = Symbol();
export class MapWithDefault<K, V> extends Map<K | typeof DEFAULT, V> {
get(key: K): V {
return super.has(key)
? super.get(key)
: super.get(DEFAULT);
}
}
使用方法:
import { MapWithDefault, DEFAULT } from './mapWithDefault';
const map = new MapWithDefault<number, string>([
[404, 'Not found!'],
[403, 'Forbidden!'],
[DEFAULT, 'Unknown error'],
]);
console.log(map.get(404)); // "Not found!"
console.log(map.get(123)); // "Unknown error"
get
在 TypeScript 5.1 中无法通过类型检查,但以下方法可以:get(key: K): V { return super.get(key) ?? (super.get(DEFAULT) as V); }
- Reuben Thomas我也有这个需求,所以我创建了一个小的Map
扩展(链接在这里),并在下面也包含了。
class MapWithDefaultValues extends Map {
constructor(...args) {
super(...args);
this._defaultGetValue = undefined;
}
setDefault(value) {
this._defaultGetValue = value;
return this;
}
clearDefault() {
return this.setDefault(undefined);
}
get(key, ...rest) {
if (super.has(key)) {
return super.get(key);
}
let [defaultValue] = rest;
// Allow for `get(key, undefined)` to override `this._defaultGetValue`
if (rest.length === 0) {
defaultValue = this._defaultGetValue;
}
return typeof defaultValue === 'function' ? defaultValue(key, this) : defaultValue;
}
}
使用函数作为defaultValue可以让您拥有计算的默认值。
const doubles = new MapWithDefaultValues([[1, 'two'], [2, 'four']]);
doubles.setDefault(key => typeof key === 'number' ? key * 2 : undefined);
doubles.get(1) // 'two'
doubles.get(5) // 10 (computed default)
传递给默认值的第二个参数还允许您使用整个底层的Map
,因此您可以执行诸如获取默认值并且set()
在Map
上。
const caps = new MapWithDefaultValues();
caps.size; // 0
caps.get('a', (key, map) => {
const result = String(key).toUpperCase();
map.set(key, result);
return result;
}); // 'A'
caps.size; // 1
[0]
处没有任何元素。 - Adrian Wm[whatever] || []
的翻译是:如果m[whatever]
存在,就返回它;否则返回空数组[]
。 - Andrew Li