С недавних пор в ASP.NET Core появилась встроенная поддержка кэширования. Пропустим шаги по его добавлению в ваше приложение, и перейдём сразу к сути.
Взаимодействовать с кэшем можно следующим образом:
// Добавление в кэш
_cache.Set(key, foo);
// Извлечение
_cache.Get<Foo>(key);
Какой тип данных лучше всего использовать в качестве ключа? Если вы думаете, что можно использовать значения типа
int, long или структуры, то я вас разочарую.📌 Во-первых, методы
Set, Get, GetOrCreate, TryGetValue принимают ключ типа object. То есть даже если ваш ключ имеет тип значения, он всё равно будет упакован. Следовательно, сэкономить на аллокациях в куче не получится.public static TItem Set<TItem>(
this IMemoryCache cache,
object key,
TItem value)
public static TItem? Get<TItem>(
this IMemoryCache cache,
object key)
public static TItem? GetOrCreate<TItem>(
this IMemoryCache cache,
object key,
Func<ICacheEntry, TItem> factory)
public static bool TryGetValue<TItem>(
this IMemoryCache cache,
object key,
out TItem? value)
📌 Во-вторых, если вы всё же решили использовать тип значения, будьте готовы к тому, что количество коллизий хэша возрастёт. Не спасут даже
record struct. Рассмотрим пример:var value = 1;
var key1 = new Key1(value);
var key2 = new Key2(value);
var key3 = new Key3(value);
var key4 = new Key4(value);
// value: 1
Console.WriteLine(
$"value: {value.GetHashCode()}");
// key1: 1
Console.WriteLine(
$"key1: {key1.GetHashCode()}");
// key2: 1
Console.WriteLine(
$"key2: {key2.GetHashCode()}");
// key3: 905109741
Console.WriteLine(
$"key3: {key3.GetHashCode()}");
// key4: -287359892
Console.WriteLine(
$"key4: {key4.GetHashCode()}");
public record struct Key1(int Id);
public record struct Key2(int Id);
public record class Key3(int Id);
public record class Key4(int Id);
Хэш код во всех случаях с типом значения равен 1. Такое поведение вызвано реализацией расчёта хэша для
record struct – возвращается хэш поля int Id, то есть само значение Id.[IsReadOnly]
[CompilerGenerated]
public override readonly int GetHashCode()
{
return EqualityComparer<int>.Default.GetHashCode(this.<Id>k__BackingField);
}
И поскольку под капотом у
IMemoryCache находится самый обычный Dictionary, то при добавлении элементов с такими ключами произойдёт коллизия. Да, кэш всё ещё будет работать, но не так быстро, как это требуется.Можно переопределить
GetHashCode для структур, но зачем, если в конечном счёте значение всё равно будет упаковано.С
record class ситуация иная, поскольку хэш рассчитывается по-другому. Помимо значения Id учитывается ещё и тип.[CompilerGenerated]
public override int GetHashCode() {
return EqualityComparer<Type>
.Default.GetHashCode(
this.EqualityContract) * -1521134295
+ EqualityComparer<int>
.Default.GetHashCode(
this.<Id>k__BackingField);
}
[CompilerGenerated]
protected virtual Type EqualityContract {
[CompilerGenerated]
get {
return typeof (Key4);
}
}
📌 Вывод
Я не знаю, почему в .NET не реализовали дженерик
IMemoryCache, который бы позволил эффективно использовать структуры. Наверно на это были причины. Но до тех пор, не используйте значимые типы в качестве ключа для IMemoryCache, т.к. производительность такого решения хуже, чем со ссылочными типами.