אחת הדוגמאות השכיחות ל-Generics היא ה-List. אנחנו יכולים ליצור List מסוגים שונים.
List<int> numbers = [1, 2, 3];
List<string> strings = ["a", "b", "c"];
int שונה מ-string ובכל זאת ולשניהם אנחנו משתמשים ב-List. אם נשים ערך מסוג שונה מזה של סוג המוגדר, הקומפיילר יצעק.
נוכל גם להגדיר List מסוג object וזה יעבוד, כי כל דבר הוא object:
List<object> objects = ["a", 4];
הגדרה כזאת לא תהיה יעילה כי הערכים יומרו לאובייקטים ויבזבזו זיכרון וגם אין לי בדיקה של סוג שנותנת שכבת אבטחה למידע שנכנס ל-List.
כדי לטפל ביעילות ובבדיקת סוג, במקום שיהיה לנו סוג אחד של List מסוג object ואליו נוכל לשלוח כל type שנרצה, יש לנו אפשרות להגדיר List מסוגים שונים.
הגדרה Generics
Generics הם הדרך ליצור classes, פונקציות ואוספים שיכולים לעבוד עם כל סוג של data ולספק בדיקת type. זה מאפשר ליצור קוד פעם אחת ולהשתמש בו במקרים שונים.
Generics מאפשרים לכתוב קוד גמיש ושניתן להשתמש בו שימוש חוזר והם מונעים שגיאות של הכנסת type לא נכון, מה שיכול להתרחש כשמשתמשים בסוג של object.
פונקציה גנרית
נסתכל על הפונקציה TypeChecker שמקבלת משתנה מסוג כלשהו. את הסוג שהפונקציה עובדת איתו מסמנים בתןך <>. כברירת מחדל מוסכמת, האות שנשתמש בה ל-Generics היא T.
void TypeChecker<T>(T value)
{
Console.WriteLine("Type: " + typeof(T));
Console.WriteLine("Value: " + value);
}
TypeChecker מסוג int, יקבל כערך לפונקציה type של int וכן לגבי שאר סוגי המשתנים.
אם נגדיר משתנה של מבנה כלשהו ונשלח אותו לפונקציה, היא גם תעבוד.
record PersonRecord(string firstName, string lastName);
TypeChecker(new PersonRecord("fn", "ln");
הפונקציה מאפשרת לנו לשלוח אליה כל type ולקבל ממנה את התוצאה שאנחנו רוצים. אם לא היינו משתמשים בפונ' גנרית היינו צריכים להגדיר פונקציות שונות לכל type למשל: TypeCheckerString, TypeCheckerInt וכן הלאה, שמקבלות את סוג הנתונים שמתאים להן. ואז גם הייתי צריכה להתכונן לכל סוג אפשרי, כשיש כאלה שלא ידועים לי עדיין.
Generic Class
public class BetterList<T>
{
private List<T> data = new();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
העברתי T ל-class ובקשתי ליצור לי List של T. זה מייצר לי רשימה על סמך ה-type שהעברתי. אחר כך נדפיס את הערך שנוסף.
עכשיו נשתמש ב-class שיצרנו.
BetterList<int> betterNumbers = new();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new();
betterNumbers.AddToList(new PersonRecord("fn", "ln"));
Generic Interface
public interface IImportance<T>
{
T MostImportant(T a, T b);
}
public class EvaluateImportance : IImportance<int>, IImportance<string>
{
public int MostImportant(int a, int b)
{
...
}
public string MostImportant(string a, string b)
{
...
}
}
אפשר לממש Interface גנרי ולממש אותו על types שונים.
Constraints
נניח שאני רוצה ליצור class גנרי, אבל אני לא רוצה שיהיה אפשר להשתמש בו בכל סוג של משתנה. או שאני רוצה לייצר הגבלות אחרות ל-class, יש אפשרות לייצר Constraints.
// Constraints: new() -> this class must have an empty constructor
public class SomeClass<T> where T : new()
{
}
// Constraints: class -> this class must be of type class
public class SomeClass<T> where T : class
{
}
// Constraints: class? -> this class must a nullable class
public class SomeClass<T> where T : class?
{
}
// Constraints: notnull-> this class must not be null
public class SomeClass<T> where T : notnull
{
}
// Constraints: BetterList<T>-> this class must be of BetterList class
public class SomeClass<T> where T : BetterList<T>
{
}
// Constraints: The class can be of 2 types and they
// need to be the same or u derived from T
public class SomeClass<T, U> where T : U
{
}
// Constraints: T has to be INumber
// The operation x+y can be implementer, only because T is INumber
// Otherwise you can't use + between 2 objects.
public class SomeClass<T> where T : INumber<T>
{
public T Add(T x, T y)
{
return x+y;
}
}
דוגמאות מהחיים
נראה שימוש לדוגמא ב-generics באתר של מסחר אלקטרוני.
Repository Pattern
נראה יישום של generic repository. שכבת repository משמשת לטיפול באינטראקציה בין האפליקציה ל-DB. במקום ליצור repositories לכל מודל בנפרד נבנה אחד גנרי.
public interface IRepository<T> where T : class
{
IEnumerable<T> GetAll();
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public IEnumerable<T> GetAll()
{
return _dbSet.ToList();
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
public void Add(T entity)
{
_dbSet.Add(entity);
_context.SaveChanges();
}
public void Update(T entity)
{
_dbSet.Update(entity);
_context.SaveChanges();
}
public void Delete(int id)
{
var entity = _dbSet.Find(id);
if (entity != null)
{
_dbSet.Remove(entity);
_context.SaveChanges();
}
}
}
עכשיו אפשר לטפל בפעולות DB באופן פשוט.
var productRepository = new Repository<Product>(dbContext);
var orderRepository = new Repository<Order>(dbContext);
Service Response Wrapper
באפליקציות שמשתמשות ב-API אנחנו שולחים חזרה את ה-response שאנחנו מקבלים. אפשר ליצור response גנרי.
public class ServiceResponse<T>
{
public T Data { get; set; }
public bool Success { get; set; } = true;
public string Message { get; set; } = string.Empty;
public ServiceResponse() { }
public ServiceResponse(T data)
{
Data = data;
}
}
שימוש ב-controllers שמחזירים את ה-response.
public ServiceResponse<Product> GetProductById(int id)
{
var product = _productRepository.GetById(id);
if (product == null)
{
return new ServiceResponse<Product>
{
Success = false,
Message = "Product not found"
};
}
return new ServiceResponse<Product>(product);
}
Paging and Filtering
דפדוף מטפל בהחזרה מדורגת של כמות גדולה של רשומות. אפשר ליצור דפדף גנרי.
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; }
public int TotalCount { get; set; }
public int PageSize { get; set; }
public int CurrentPage { get; set; }
public PagedResult(IEnumerable<T> items, int totalCount, int pageSize, int currentPage)
{
Items = items;
TotalCount = totalCount;
PageSize = pageSize;
CurrentPage = currentPage;
}
}
public class PagingHelper
{
public static PagedResult<T> GetPagedResult<T>(IQueryable<T> query, int page, int pageSize)
{
var totalCount = query.Count();
var items = query.Skip((page - 1) * pageSize).Take(pageSize).ToList();
return new PagedResult<T>(items, totalCount, pageSize, page);
}
}
שימוש.
var pagedProducts = PagingHelper.GetPagedResult(_context.Products, page: 1, pageSize: 10);
var pagedOrders = PagingHelper.GetPagedResult(_context.Orders, page: 2, pageSize: 5);