R包的尽头是 C/C++

该博客介绍了如何利用C/C++编写R包,以kaori为例,这是一个用于统计fastq文件中barcode数量的工具,允许指定最多错配次数。kaori依赖Zlib库进行压缩。博主分享了如何调用C/C++代码来构建R包,以及学习CMake和C++编程的相关资源。

疑似 singleR 包的作者的github

  • https://github.com/LTLA?page=2&tab=repositories

他主要使用的语言是 C/R/C++.

R包的尽头是C/C++

barcode counting lib in C++

  • https://github.com/LTLA/kaori
  • https://ltla.github.io/kaori/

这个包是统计 fastq 文件中 barcode 的数量的。
可以指定最多错配个数。
压缩依赖 Zlib 库。

帮助页面: https://ltla.github.io/kaori/

有待测试:

  • 如何调用C/C++代码写R包(.C()和.Call()接口 ): https://zhuanlan.zhihu.com/p/525648754
  • 还需要了解 CMake: https://github.com/Kitware/CMake
  • 学习 C++ 笔记: https://blog.youkuaiyun.com/wangjunliang/article/details/126533710

R data.table 主要是C

  • https://github.com/Rdatatable/data.table

李恒 的C代码

李恒的博客中提到,R和python大概比C/C++慢50倍,除非找到其他高效语言写的库,否则,用C/C++一分钟的需要一个小时,一个小时的需要浪费两天。
另一个问题是python的部署问题,很容易包冲突。而C/C++可以编译为不依赖的二进制文件,方便用户使用。

如果拿枪指着脑袋,非要选一个语言推荐,大神推荐内存安全的语言Rust,因为Linux内核也用了,Google和MS Azure甚至白宫也在推。
缺点:Rust学习曲线太陡峭!
但是,看起来的C,用到不经常出现 segmentation fault 也需要好几年的练习。而现代C++和C十分不同,它是一个添加了大量现代特性的巨兽+塞了数十年历史代码的最复杂语言。掌握C++需要更多努力。
如果不需要内存安全,只需要快速,Go也是第二个选择。
其他语言,要么太小众,要么设计不好,要么只在工业界使用(Swift, C#),都可以自己尝试,但不推荐给计算生物学家。
大神自己短期内不转Rust,还继续使用 C。

  • C++之父的网站: https://www.stroustrup.com/quotes.html

c++读书笔记

  • https://dawneve.github.io/learnCpp/#/
根据我提供的控件源码,写出一个更好的控件 #ifndef _CONSOLE_H_ #define _CONSOLE_H_ #define _CRT_SECURE_NO_WARNINGS #define NO_WARN_MBCS_MFC_DEPRECATION #include <SDKDDKVer.h> //#include <afxwin.h> #include <afxcmn.h> #include <concurrent_vector.h> #define CONSOLECOLOR_BLACK "\x1E\x1D" //字体 黑色 #define CONSOLECOLOR_RED "\x1E\x1F" //字体 红色 #define CONSOLECOLOR_GREEN "\x1E\x20" //字体 绿色 #define CONSOLECOLOR_YELLOW "\x1E\x21" //字体 黄色 #define CONSOLECOLOR_BLUE "\x1E\x22" //字体 蓝色 #define CONSOLECOLOR_PURPLE "\x1E\x23" //字体 紫色 #define CONSOLECOLOR_CYAN "\x1E\x24" //字体 青色 #define CONSOLECOLOR_DEF "\x1E\x25" //字体 默认色 class ScrollBar; struct LineInfo { DWORD m_dwStrLen;//行内容的长度 DWORD m_dwHead;//行首的sel游标 DWORD m_dwMaxSelWnd;//当前控件x轴尽头的sel DWORD m_dwTail;//行尾的sel int m_xPos;//行首的x开始坐标 int m_yPos;//行首的y开始坐标 DWORD m_dwMaxPixl;//此行内容占最大的像素 int* m_pDx; ~LineInfo() { if (m_pDx)delete[]m_pDx; } }; class Console: public CRichEditCtrl { public: Console(CWnd* _pParent); ~Console(); template<class ...Args> void Printf(const char* pformat, Args... args) { int len = std::snprintf(nullptr, 0, pformat, args...); if (len > 0) { char* szBuf = new char[len+1]; std::snprintf(szBuf, len+1, pformat, args...); split(szBuf); delete[]szBuf; } } private: void split(const char* _szText); void printf(const char* _szText); private: CDC* m_pDC; CRect m_rcWnd; BOOL m_bIsTrack; CFont m_Font;//默认字体,只是方便计算字体占屏幕长度使用的 int m_nFontSize;//默认字体大小(单位Twips) 如220Twip = 220/20 = 11磅 CHARFORMAT2 m_CF;//最终字体的数据 concurrency::concurrent_vector<LineInfo> m_vecLineInfo; ScrollBar* m_pHScroll; SCROLLINFO m_infoHScroll; DWORD m_pixCurMaxChars; ScrollBar* m_pVScroll; SCROLLINFO m_infoVScroll; int m_iLineHeight; int m_iStartPos_x; int m_iPrePos_x; bool m_bIsCapture; CHARRANGE m_StartSel; public: DECLARE_MESSAGE_MAP() afx_msg void OnEnSelchange(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnEnMsgfilter(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnMouseLeave(); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); }; extern Console* pConsole; #define console(_format, ...)pConsole->Printf(_format, ##__VA_ARGS__); #endif#include "Console.h" #include <io.h> #include <thread> #include "ScrollBar/ScrollBar.h" Console* pConsole = NULL; Console::Console(CWnd* _pParent) : m_nFontSize(10 * 10), m_bIsTrack(FALSE), m_pHScroll(NULL), m_pVScroll(NULL), m_pixCurMaxChars(0), m_iPrePos_x(0), m_iStartPos_x(0), m_bIsCapture(FALSE) { if (_pParent) { const char* szFontName = "lucida consoles"; //Arial _pParent->GetWindowRect(&m_rcWnd); m_rcWnd = {10, 0, m_rcWnd.Width()-15, m_rcWnd.Height()-15};//682 //| WS_HSCROLL | WS_VSCROLL if (CreateEx(NULL, WS_CHILD | WS_VISIBLE | ES_LEFT | ES_WANTRETURN | ES_MULTILINE | ES_READONLY | ES_AUTOHSCROLL | ES_AUTOVSCROLL, m_rcWnd, _pParent, 0))// { m_pDC = GetDC(); SetBackgroundColor(false, RGB(30, 30, 30));//设置背景颜色 LimitText(32767);//系统默认设置是 32767 = 31.999kb 只能限制用户的输入 //创建新字体数据,此字体只是方便计算屏幕显示的长度用的。最终字体数据以 CHARFORMAT2 为准 //以十分之一磅为单位。例如,如果你想要一个12磅的字体,你应该传递120作为这个参数。 m_Font.CreatePointFont(m_nFontSize, szFontName);// 100/10 =10磅,相当于200Twip CreatePointFont用的单位是Point, 是磅的10倍,刚好是Twips的一半 SetFont(&m_Font);//设置字体 m_pDC->SelectObject(&m_Font);//dc选择当前字体 /* 1缇等于1/1440 英寸或1/567厘米 缇(Twips) (缇:计量单位,等于“磅”的 1/20,英寸的 1/1440。一厘米有 567 缇。 像素(Pixels):监视器或打印机分辨率的最小单位 右键单击桌面,选择属性,选择“设置”选卡,单击高级按钮。 里面出现DPI设置。一般为“正常尺寸(96 DPI)”。 DPI的意思就是 DPI(Dots per Inch)。因此我们可以得到如下换算公式 1 Pixel = 1440 TPI / 96 DPI = 15 Twips 1 Twip = 96 DPI / 1440 TPI = 0.0666667 Pixels 磅:指打印的字符的高度的度量单位, 1磅等于1/72英寸,或大约等于1厘米的 1/28。 英寸:2.54 厘米 一般情况下:1厘米=8505像素 20Twip = 1磅 = 20*0.0666667 = 1.333334 Pixels */ LRESULT FontStyle = 0; FontStyle = SendMessage(EM_GETLANGOPTIONS, 0, 0); if (FontStyle & IMF_DUALFONT) { FontStyle &= ~IMF_DUALFONT; //"<CRichEdit_Ctrl::CRichEdit_Ctrl>成功去掉IMF_DUALFONT"; } SendMessage(EM_SETLANGOPTIONS, 0, (LPARAM)FontStyle); /* PARAFORMAT2 pf; ZeroMemory(&pf, sizeof(PARAFORMAT2)); pf.cbSize = sizeof pf; pf.dwMask |= PFM_ALL; pf.bLineSpacingRule = 100; pf.dyLineSpacing = 400; pf.dySpaceAfter = 400; pf.dySpaceBefore = 400; pf.dxStartIndent = 100; pf.dxRightIndent = 100; SetParaFormat(pf); */ TEXTMETRIC tm; ::GetTextMetrics(m_pDC->GetSafeHdc(), &tm); m_iLineHeight = tm.tmHeight + (tm.tmDescent*2);//取得行高 //准确的字体数据 memset(&m_CF, 0, sizeof(m_CF)); //StrCpyA(m_CF.szFaceName, szFontName);//设置字体 //m_CF.cbSize = sizeof(CHARFORMAT2); m_CF.dwMask = CFM_EFFECTS | CFM_COLOR; m_CF.dwEffects = 0; m_CF.crTextColor = RGB(165, 165, 165);//设置颜色 //m_CF.yHeight = m_nFontSize;//除20得到磅 如 200/20 = 10磅 //默认的字符磅数 SetDefaultCharFormat(m_CF);//设置字体 SetEventMask(GetEventMask() | ENM_CHANGE | ENM_SELCHANGE | ENM_SCROLL | ENM_MOUSEEVENTS); //创建滚动动 m_pHScroll = new ScrollBar(); if (m_pHScroll->Create(SB_HORZ, CRect(m_rcWnd.left, m_rcWnd.Height(), m_rcWnd.right, m_rcWnd.Height()+15), _pParent)) { m_pHScroll->SetTarget(this); memset(&m_infoHScroll, 0, sizeof m_infoHScroll); m_infoHScroll.cbSize = sizeof m_infoHScroll; m_infoHScroll.fMask = SIF_ALL; m_infoHScroll.nPage = m_rcWnd.Width(); } m_pVScroll = new ScrollBar(); if (m_pVScroll->Create(SB_VERT, CRect(m_rcWnd.right, m_rcWnd.top, m_rcWnd.right+15, m_rcWnd.bottom), _pParent)) { m_pVScroll->SetTarget(this); memset(&m_infoVScroll, 0, sizeof m_infoVScroll); m_infoVScroll.cbSize = sizeof m_infoVScroll; m_infoVScroll.fMask = SIF_ALL; m_infoVScroll.nPage = m_rcWnd.Height(); } } } } Console::~Console() { m_Font.DeleteObject(); if (m_pHScroll)delete m_pHScroll; if (m_pVScroll)delete m_pVScroll; for (concurrency::concurrent_vector<LineInfo>::iterator iter = m_vecLineInfo.begin(); iter != m_vecLineInfo.end(); iter++) { if (iter->m_pDx) { delete[] iter->m_pDx; iter->m_pDx = NULL; } } } /* *0x1E RS (Record Separator) 记录分离符 属性代码 颜色 29 黑色 = 0x1D 31 红色 = 0x1F 32 绿色 = 0x20 33 黄色 = 0x21 34 蓝色 = 0x22 35 紫色 = 0x23 36 青色 = 0x24 */ void Console::printf(const char* _szText) { const char* szText = _szText; char byteColor = 0; //如果\n会替换成\r int indexLine = LineFromChar(-1);//取出当前行号 long lStart = 0; long lEnd = 0; GetSel(lStart, lEnd);//取得当前的光标位置 CString szStart; GetTextRange(lStart - 1, lEnd, szStart);//当前光标向后选择一位字符,并取出此字符 if (szStart == '\r' || szStart == "")//判断字符是否行头 { CPoint point = PosFromChar(lEnd); LineInfo info; memset(&info, 0, sizeof info); info.m_dwHead = lEnd; info.m_xPos = point.x; info.m_yPos = point.y; m_vecLineInfo.push_back(info);//保存行头的光标位置 } if (_szText[0] == '\x1E') { switch (_szText[1]) { case '\x1D': { m_CF.crTextColor = RGB(0, 0, 0); break; } case '\x1F': { m_CF.crTextColor = RGB(255, 0, 0); break; } case '\x20': { m_CF.crTextColor = RGB(0, 255, 0); break; } case '\x21': { m_CF.crTextColor = RGB(255, 255, 0); break; } case '\x22': { m_CF.crTextColor = RGB(0, 0, 255); break; } case '\x23': { m_CF.crTextColor = RGB(199, 0, 106); break; } case '\x24': { m_CF.crTextColor = RGB(0, 255, 255); break; } default: { m_CF.crTextColor = RGB(165, 165, 165); break; } } szText = _szText+2; SetSelectionCharFormat(m_CF); } ReplaceSel(szText); m_CF.crTextColor = RGB(165, 165, 165);//重置回原始颜色 SetSelectionCharFormat(m_CF); LineInfo* pInfo = &(m_vecLineInfo[indexLine]); //尝试取出控件尽头x轴末尾是否有字符存在 GetSel(lStart, lEnd);//取得当前的光标位置 GetTextRange(lEnd - 1, lEnd, szStart);//当前光标向后选择一位字符,并取出此字符 bool bIsEnter = false; if (szStart == '\r') { //已经完成换行操作 bIsEnter = true; pInfo->m_dwTail = lEnd - 1; GetTextRange(pInfo->m_dwHead, pInfo->m_dwTail, szStart);//取出本行内容 CPoint point = PosFromChar(pInfo->m_dwTail); pInfo->m_dwMaxPixl = point.x-1;//此行文本的最大像素 int len = szStart.GetLength(); int iFit = 0; CSize size = { 0, 0 }; if (pInfo->m_pDx) { delete [] pInfo->m_pDx; pInfo->m_pDx = NULL; } pInfo->m_pDx = new int[len]; pInfo->m_dwStrLen = len; ::GetTextExtentExPoint(m_pDC->GetSafeHdc(), szStart, len, point.x, &iFit, pInfo->m_pDx, &size);//计算出本行每个字符所占的像素,并把每个字符的像素存在m_pDx数组里面 pInfo->m_yPos = indexLine * m_iLineHeight; } else { pInfo->m_dwTail = lEnd; CPoint point = PosFromChar(pInfo->m_dwTail); pInfo->m_dwMaxPixl = point.x;//此行文本的最大像素 } //重置水平滚动条信息 if (pInfo->m_dwMaxPixl > (DWORD)m_rcWnd.Width() && pInfo->m_dwMaxPixl > m_pixCurMaxChars) { m_pixCurMaxChars = pInfo->m_dwMaxPixl; m_infoHScroll.nMax = m_pixCurMaxChars; m_pHScroll->SetScrollInfo(m_infoHScroll); } if (bIsEnter) { //已经完成换行操作 if (((indexLine + 1) * m_iLineHeight) > m_rcWnd.Height()) { m_infoVScroll.nMax = ((indexLine + 1) * m_iLineHeight); m_pVScroll->SetScrollInfo(m_infoVScroll); m_pVScroll->SetScrollPos(m_infoVScroll.nMax - m_rcWnd.Height());//向下滚动 } LineScroll(1); } } void Console::split(const char* _szText) { const char* szCurText = _szText; const char* szOldText = _szText; char* szPreText = NULL; int len = NULL; const char* szClrText = NULL; if (_szText[0] != '\x1E') { bool bIsFined = false; while ((szClrText = strchr(szCurText, '\x1E'))) { bIsFined = true; len = szClrText - szOldText; szPreText = new char[len + 1]; szPreText[len] = 0; memcpy(szPreText, szOldText, len); Console::printf(szPreText); delete[]szPreText; szCurText = szClrText + 1; szOldText = szClrText; } if (bIsFined) { if(szOldText)Console::printf(szOldText); } else { Console::printf(_szText); } } else { bool bIsFined = false; szCurText = _szText + 1; while ((szClrText = strchr(szCurText, '\x1E'))) { bIsFined = true; len = szClrText - szOldText; szPreText = new char[len + 1]; szPreText[len] = 0; memcpy(szPreText, szOldText, len); Console::printf(szPreText); delete[]szPreText; szCurText = szClrText + 1; szOldText = szClrText; } if (szOldText) { Console::printf(szOldText); } } } BEGIN_MESSAGE_MAP(Console, CRichEditCtrl) ON_NOTIFY_REFLECT(EN_SELCHANGE, &Console::OnEnSelchange) ON_NOTIFY_REFLECT(EN_MSGFILTER, &Console::OnEnMsgfilter) ON_WM_MOUSELEAVE() ON_WM_VSCROLL() END_MESSAGE_MAP() void Console::OnEnSelchange(NMHDR* pNMHDR, LRESULT* pResult) { SELCHANGE* pSelChange = reinterpret_cast<SELCHANGE*>(pNMHDR); // TODO: 控件将不发送此通知,除非您重写 // CRichEditCtrl::OnInitDialog() 函数,以将 EM_SETEVENTMASK 消息发送 // 到该控件,同时将 ENM_SELCHANGE 标志“或”运算到 lParam 掩码中。 // TODO: 在此添加控件通知处理程序代码 int iLineIndex = -1; iLineIndex = LineFromChar(pSelChange->chrg.cpMax); if (iLineIndex != -1 && iLineIndex < (int)m_vecLineInfo.size()) { CHARRANGE LineStartSel; LineInfo* pInfo = &m_vecLineInfo[iLineIndex]; switch (pSelChange->seltyp) { case 0://鼠标点击在某处文本的某个sel中 { //开始存储此行的选择sel起始位置 m_StartSel = pSelChange->chrg; break; } case CBN_EDITCHANGE://鼠标左键按住拖动选择文本 { //通过m_StartSel算出本行的选择起始位置 CPoint point = PosFromChar(m_StartSel.cpMin);//算出sel处于控件内坐标 int x = point.x; point.y = pInfo->m_yPos;//替换上本行的y坐标 LineStartSel.cpMin = CharFromPos(point);//算出本行起始位置 LineStartSel.cpMax = LineStartSel.cpMin; point = PosFromChar(LineStartSel.cpMin); if (point.x < x - 5) { //outputdebug_default("行号:%d 检测到起始位置异常,和原位差太远", iLineIndex); LineStartSel.cpMin = 0; LineStartSel.cpMax = 0; if (point.x > m_rcWnd.Width()) { m_pHScroll->SetScrollPos(point.x - m_rcWnd.Width());//通知滚动条 } else { m_pHScroll->SetScrollPos(0); } } if (pSelChange->chrg.cpMax > LineStartSel.cpMax) { //outputdebug_default("行号:%d 从左向右选择文本: %d", iLineIndex, pSelChange->chrg.cpMax); CString str; GetTextRange(pInfo->m_dwHead, pSelChange->chrg.cpMax, str); int len = str.GetLength(); if (--len > -1 && len < (int)pInfo->m_dwStrLen) { //outputdebug_default("行号:%d, 当前光标所占像素: %d", iLineIndex, pInfo->m_pDx[len]); int pix = pInfo->m_pDx[len]; if (pix > m_rcWnd.Width()) { //outputdebug_default("行号:%d, 当前光标所占像素超出控件长度: %d", iLineIndex, pix); m_pHScroll->SetScrollPos(pix - m_rcWnd.Width());//通知滚动条 } } else { if (--len > -1 && pInfo->m_dwStrLen == 0) { //此行末尾没有/r回车符 CPoint point = PosFromChar(pSelChange->chrg.cpMax); if (point.x > m_rcWnd.Width()) { m_pHScroll->SetScrollPos(point.x - m_rcWnd.Width());//通知滚动条 } else { m_pHScroll->SetScrollPos(0); } } else { int i = 0; } } } else if (pSelChange->chrg.cpMax != pInfo->m_dwHead) { //outputdebug_default("行号:%d 从右向左选择文本: head:%d start:%d, min:%d max:%d", iLineIndex, pInfo->m_dwHead, LineStartSel.cpMax, pSelChange->chrg.cpMin, pSelChange->chrg.cpMax); if (pSelChange->chrg.cpMin == pInfo->m_dwHead) { m_pHScroll->SetScrollPos(0); } } else { //outputdebug_default("x行号:%d 从右向左选择文本: head:%d start:%d, min:%d max:%d", iLineIndex, pInfo->m_dwHead, LineStartSel.cpMax, pSelChange->chrg.cpMin, pSelChange->chrg.cpMax); if (pInfo->m_dwHead == pSelChange->chrg.cpMax)//本行头部和上一行行尾重复,定位回上一行 { if (--iLineIndex > -1) { pInfo = &m_vecLineInfo[iLineIndex]; CString str; GetTextRange(pInfo->m_dwHead, pSelChange->chrg.cpMax, str); int len = str.GetLength(); //outputdebug_default("行号:%d, 当前光标所占像素: %d len:%d", iLineIndex, pInfo->m_pDx[len-2], len); if (--len > -1 && len <= (int)pInfo->m_dwStrLen) { int pix = pInfo->m_pDx[len-1]; if (pix > m_rcWnd.Width()) { //outputdebug_default("行号:%d, 当前光标所占像素超出控件长度: %d", iLineIndex, pix); //m_pHScroll->SetScrollPos(pix - m_rcWnd.Width());//通知滚动条 } } } } } break; } } } //outputdebug_default("min:%d max:%d", pSelChange->chrg.cpMin, pSelChange->chrg.cpMax); *pResult = 0; } void Console::OnEnMsgfilter(NMHDR* pNMHDR, LRESULT* pResult) { MSGFILTER* pMsgFilter = reinterpret_cast<MSGFILTER*>(pNMHDR); // TODO: 控件将不发送此通知,除非您重写 // CRichEditCtrl::OnInitDialog() 函数,以将 EM_SETEVENTMASK 消息发送 // 到该控件,同时将 ENM_KEYEVENTS 或 ENM_MOUSEEVENTS 标志 //“或”运算到 lParam 掩码中。 // TODO: 在此添加控件通知处理程序代码 switch(pMsgFilter->msg) { case WM_MOUSEMOVE: { /* if (pMsgFilter->wParam == MK_LBUTTON) { int xPos = GET_X_LPARAM(pMsgFilter->lParam); int yPos = GET_Y_LPARAM(pMsgFilter->lParam); //outputdebug_default("x: %d, y:%d",xPos, yPos); if (m_bIsCapture) { if (xPos > m_iStartPos_x) { //outputdebug_default("正在从左向右选择文本."); } else { //outputdebug_default("正在从右向左选择文本."); } m_iPrePos_x = xPos; } }*/ if (!m_bIsTrack) { m_bIsTrack = TRUE; TRACKMOUSEEVENT tme; tme.cbSize = sizeof(tme); tme.hwndTrack = m_hWnd; tme.dwFlags = TME_LEAVE | TME_HOVER; tme.dwHoverTime = 1;// 鼠标在按钮上停留超过 1ms ,才认为成功 _TrackMouseEvent(&tme); } break; } case WM_LBUTTONDOWN: { if (pMsgFilter->wParam == MK_LBUTTON) { int xPos = GET_X_LPARAM(pMsgFilter->lParam); int yPos = GET_Y_LPARAM(pMsgFilter->lParam); m_iPrePos_x = xPos; m_iStartPos_x = xPos; m_bIsCapture = TRUE; //outputdebug_default("x: %d, y:%d", xPos, yPos); } break; } case WM_LBUTTONUP: { if (pMsgFilter->wParam == MK_LBUTTON) { m_bIsCapture = FALSE; } break; } } *pResult = 0; } void Console::OnMouseLeave() { // TODO: 在此添加消息处理程序代码和/或调用默认值 m_bIsTrack = FALSE; ::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW)); CRichEditCtrl::OnMouseLeave(); } void Console::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CRichEditCtrl::OnVScroll(nSBCode, nPos, pScrollBar); }
07-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值